mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-06-30 02:13:33 -04:00
Support removing multiple plugins at once in the CLI (#69063)
Closes #66476. Add support for removing multiple plugins at the same time to `elasticsearch-plugin`. Also change references from "plugin name" to "plugin id", to align better with the installer class.
This commit is contained in:
parent
8db1c9ce72
commit
119ba5ac65
3 changed files with 110 additions and 38 deletions
|
@ -23,16 +23,19 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.StringJoiner;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE;
|
import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A command for the plugin CLI to remove a plugin from Elasticsearch.
|
* A command for the plugin CLI to remove plugins from Elasticsearch.
|
||||||
*/
|
*/
|
||||||
class RemovePluginCommand extends EnvironmentAwareCommand {
|
class RemovePluginCommand extends EnvironmentAwareCommand {
|
||||||
|
|
||||||
|
@ -44,16 +47,16 @@ class RemovePluginCommand extends EnvironmentAwareCommand {
|
||||||
private final OptionSpec<String> arguments;
|
private final OptionSpec<String> arguments;
|
||||||
|
|
||||||
RemovePluginCommand() {
|
RemovePluginCommand() {
|
||||||
super("removes a plugin from Elasticsearch");
|
super("removes plugins from Elasticsearch");
|
||||||
this.purgeOption = parser.acceptsAll(Arrays.asList("p", "purge"), "Purge plugin configuration files");
|
this.purgeOption = parser.acceptsAll(Arrays.asList("p", "purge"), "Purge plugin configuration files");
|
||||||
this.arguments = parser.nonOptions("plugin name");
|
this.arguments = parser.nonOptions("plugin id");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void execute(final Terminal terminal, final OptionSet options, final Environment env) throws Exception {
|
protected void execute(final Terminal terminal, final OptionSet options, final Environment env) throws Exception {
|
||||||
final String pluginName = arguments.value(options);
|
final List<String> pluginIds = arguments.values(options);
|
||||||
final boolean purge = options.has(purgeOption);
|
final boolean purge = options.has(purgeOption);
|
||||||
execute(terminal, env, pluginName, purge);
|
execute(terminal, env, pluginIds, purge);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,55 +64,91 @@ class RemovePluginCommand extends EnvironmentAwareCommand {
|
||||||
*
|
*
|
||||||
* @param terminal the terminal to use for input/output
|
* @param terminal the terminal to use for input/output
|
||||||
* @param env the environment for the local node
|
* @param env the environment for the local node
|
||||||
* @param pluginName the name of the plugin to remove
|
* @param pluginIds the IDs of the plugins to remove
|
||||||
* @param purge if true, plugin configuration files will be removed but otherwise preserved
|
* @param purge if true, plugin configuration files will be removed but otherwise preserved
|
||||||
* @throws IOException if any I/O exception occurs while performing a file operation
|
* @throws IOException if any I/O exception occurs while performing a file operation
|
||||||
* @throws UserException if plugin name is null
|
* @throws UserException if pluginIds is null or empty
|
||||||
* @throws UserException if plugin directory does not exist
|
* @throws UserException if plugin directory does not exist
|
||||||
* @throws UserException if the plugin bin directory is not a directory
|
* @throws UserException if the plugin bin directory is not a directory
|
||||||
*/
|
*/
|
||||||
void execute(Terminal terminal, Environment env, String pluginName, boolean purge) throws IOException, UserException {
|
void execute(Terminal terminal, Environment env, List<String> pluginIds, boolean purge) throws IOException, UserException {
|
||||||
if (pluginName == null) {
|
if (pluginIds == null || pluginIds.isEmpty()) {
|
||||||
throw new UserException(ExitCodes.USAGE, "plugin name is required");
|
throw new UserException(ExitCodes.USAGE, "At least one plugin ID is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// first make sure nothing extends this plugin
|
ensurePluginsNotUsedByOtherPlugins(env, pluginIds);
|
||||||
List<String> usedBy = new ArrayList<>();
|
|
||||||
|
for (String pluginId : pluginIds) {
|
||||||
|
checkCanRemove(env, pluginId, purge);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String pluginId : pluginIds) {
|
||||||
|
removePlugin(env, terminal, pluginId, purge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensurePluginsNotUsedByOtherPlugins(Environment env, List<String> pluginIds) throws IOException, UserException {
|
||||||
|
// First make sure nothing extends this plugin
|
||||||
|
final Map<String, List<String>> usedBy = new HashMap<>();
|
||||||
Set<PluginsService.Bundle> bundles = PluginsService.getPluginBundles(env.pluginsFile());
|
Set<PluginsService.Bundle> bundles = PluginsService.getPluginBundles(env.pluginsFile());
|
||||||
for (PluginsService.Bundle bundle : bundles) {
|
for (PluginsService.Bundle bundle : bundles) {
|
||||||
for (String extendedPlugin : bundle.plugin.getExtendedPlugins()) {
|
for (String extendedPlugin : bundle.plugin.getExtendedPlugins()) {
|
||||||
if (extendedPlugin.equals(pluginName)) {
|
for (String pluginId : pluginIds) {
|
||||||
usedBy.add(bundle.plugin.getName());
|
if (extendedPlugin.equals(pluginId)) {
|
||||||
|
usedBy.computeIfAbsent(bundle.plugin.getName(), (_key -> new ArrayList<>())).add(pluginId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (usedBy.isEmpty() == false) {
|
if (usedBy.isEmpty()) {
|
||||||
throw new UserException(
|
return;
|
||||||
PLUGIN_STILL_USED,
|
|
||||||
"plugin [" + pluginName + "] cannot be removed" + " because it is extended by other plugins: " + usedBy
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final Path pluginDir = env.pluginsFile().resolve(pluginName);
|
final StringJoiner message = new StringJoiner("\n");
|
||||||
final Path pluginConfigDir = env.configFile().resolve(pluginName);
|
message.add("Cannot remove plugins because the following are extended by other plugins:");
|
||||||
final Path removing = env.pluginsFile().resolve(".removing-" + pluginName);
|
usedBy.forEach((key, value) -> {
|
||||||
|
String s = "\t" + key + " used by " + value;
|
||||||
|
message.add(s);
|
||||||
|
});
|
||||||
|
|
||||||
|
throw new UserException(PLUGIN_STILL_USED, message.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkCanRemove(Environment env, String pluginId, boolean purge) throws UserException {
|
||||||
|
final Path pluginDir = env.pluginsFile().resolve(pluginId);
|
||||||
|
final Path pluginConfigDir = env.configFile().resolve(pluginId);
|
||||||
|
final Path removing = env.pluginsFile().resolve(".removing-" + pluginId);
|
||||||
|
|
||||||
terminal.println("-> removing [" + pluginName + "]...");
|
|
||||||
/*
|
/*
|
||||||
* If the plugin does not exist and the plugin config does not exist, fail to the user that the plugin is not found, unless there's
|
* If the plugin does not exist and the plugin config does not exist, fail to the user that the plugin is not found, unless there's
|
||||||
* a marker file left from a previously failed attempt in which case we proceed to clean up the marker file. Or, if the plugin does
|
* a marker file left from a previously failed attempt in which case we proceed to clean up the marker file. Or, if the plugin does
|
||||||
* not exist, the plugin config does, and we are not purging, again fail to the user that the plugin is not found.
|
* not exist, the plugin config does, and we are not purging, again fail to the user that the plugin is not found.
|
||||||
*/
|
*/
|
||||||
if ((!Files.exists(pluginDir) && !Files.exists(pluginConfigDir) && !Files.exists(removing))
|
if ((Files.exists(pluginDir) == false && Files.exists(pluginConfigDir) == false && Files.exists(removing) == false)
|
||||||
|| (!Files.exists(pluginDir) && Files.exists(pluginConfigDir) && !purge)) {
|
|| (Files.exists(pluginDir) == false && Files.exists(pluginConfigDir) && purge == false)) {
|
||||||
final String message = String.format(
|
final String message = String.format(
|
||||||
Locale.ROOT,
|
Locale.ROOT,
|
||||||
"plugin [%s] not found; run 'elasticsearch-plugin list' to get list of installed plugins",
|
"plugin [%s] not found; run 'elasticsearch-plugin list' to get list of installed plugins",
|
||||||
pluginName
|
pluginId
|
||||||
);
|
);
|
||||||
throw new UserException(ExitCodes.CONFIG, message);
|
throw new UserException(ExitCodes.CONFIG, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Path pluginBinDir = env.binFile().resolve(pluginId);
|
||||||
|
if (Files.exists(pluginBinDir)) {
|
||||||
|
if (Files.isDirectory(pluginBinDir) == false) {
|
||||||
|
throw new UserException(ExitCodes.IO_ERROR, "bin dir for " + pluginId + " is not a directory");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removePlugin(Environment env, Terminal terminal, String pluginId, boolean purge) throws IOException {
|
||||||
|
final Path pluginDir = env.pluginsFile().resolve(pluginId);
|
||||||
|
final Path pluginConfigDir = env.configFile().resolve(pluginId);
|
||||||
|
final Path removing = env.pluginsFile().resolve(".removing-" + pluginId);
|
||||||
|
|
||||||
|
terminal.println("-> removing [" + pluginId + "]...");
|
||||||
|
|
||||||
final List<Path> pluginPaths = new ArrayList<>();
|
final List<Path> pluginPaths = new ArrayList<>();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -123,11 +162,8 @@ class RemovePluginCommand extends EnvironmentAwareCommand {
|
||||||
terminal.println(VERBOSE, "removing [" + pluginDir + "]");
|
terminal.println(VERBOSE, "removing [" + pluginDir + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Path pluginBinDir = env.binFile().resolve(pluginName);
|
final Path pluginBinDir = env.binFile().resolve(pluginId);
|
||||||
if (Files.exists(pluginBinDir)) {
|
if (Files.exists(pluginBinDir)) {
|
||||||
if (Files.isDirectory(pluginBinDir) == false) {
|
|
||||||
throw new UserException(ExitCodes.IO_ERROR, "bin dir for " + pluginName + " is not a directory");
|
|
||||||
}
|
|
||||||
try (Stream<Path> paths = Files.list(pluginBinDir)) {
|
try (Stream<Path> paths = Files.list(pluginBinDir)) {
|
||||||
pluginPaths.addAll(paths.collect(Collectors.toList()));
|
pluginPaths.addAll(paths.collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
@ -180,7 +216,6 @@ class RemovePluginCommand extends EnvironmentAwareCommand {
|
||||||
// finally, add the marker file
|
// finally, add the marker file
|
||||||
pluginPaths.add(removing);
|
pluginPaths.add(removing);
|
||||||
|
|
||||||
IOUtils.rm(pluginPaths.toArray(new Path[pluginPaths.size()]));
|
IOUtils.rm(pluginPaths.toArray(new Path[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,8 +26,11 @@ import java.io.StringReader;
|
||||||
import java.nio.file.DirectoryStream;
|
import java.nio.file.DirectoryStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
import static org.hamcrest.CoreMatchers.containsString;
|
import static org.hamcrest.CoreMatchers.containsString;
|
||||||
import static org.hamcrest.CoreMatchers.not;
|
import static org.hamcrest.CoreMatchers.not;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
@ -96,10 +99,14 @@ public class RemovePluginCommandTests extends ESTestCase {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static MockTerminal removePlugin(String name, Path home, boolean purge) throws Exception {
|
static MockTerminal removePlugin(String pluginId, Path home, boolean purge) throws Exception {
|
||||||
|
return removePlugin(singletonList(pluginId), home, purge);
|
||||||
|
}
|
||||||
|
|
||||||
|
static MockTerminal removePlugin(List<String> pluginIds, Path home, boolean purge) throws Exception {
|
||||||
Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", home).build());
|
Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", home).build());
|
||||||
MockTerminal terminal = new MockTerminal();
|
MockTerminal terminal = new MockTerminal();
|
||||||
new MockRemovePluginCommand(env).execute(terminal, env, name, purge);
|
new MockRemovePluginCommand(env).execute(terminal, env, pluginIds, purge);
|
||||||
return terminal;
|
return terminal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,6 +137,23 @@ public class RemovePluginCommandTests extends ESTestCase {
|
||||||
assertRemoveCleaned(env);
|
assertRemoveCleaned(env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Check that multiple plugins can be removed at the same time. */
|
||||||
|
public void testRemoveMultiple() throws Exception {
|
||||||
|
createPlugin("fake");
|
||||||
|
Files.createFile(env.pluginsFile().resolve("fake").resolve("plugin.jar"));
|
||||||
|
Files.createDirectory(env.pluginsFile().resolve("fake").resolve("subdir"));
|
||||||
|
|
||||||
|
createPlugin("other");
|
||||||
|
Files.createFile(env.pluginsFile().resolve("other").resolve("plugin.jar"));
|
||||||
|
Files.createDirectory(env.pluginsFile().resolve("other").resolve("subdir"));
|
||||||
|
|
||||||
|
removePlugin("fake", home, randomBoolean());
|
||||||
|
removePlugin("other", home, randomBoolean());
|
||||||
|
assertFalse(Files.exists(env.pluginsFile().resolve("fake")));
|
||||||
|
assertFalse(Files.exists(env.pluginsFile().resolve("other")));
|
||||||
|
assertRemoveCleaned(env);
|
||||||
|
}
|
||||||
|
|
||||||
public void testRemoveOldVersion() throws Exception {
|
public void testRemoveOldVersion() throws Exception {
|
||||||
createPlugin(
|
createPlugin(
|
||||||
"fake",
|
"fake",
|
||||||
|
@ -237,7 +261,6 @@ public class RemovePluginCommandTests extends ESTestCase {
|
||||||
BufferedReader reader = new BufferedReader(new StringReader(terminal.getOutput()));
|
BufferedReader reader = new BufferedReader(new StringReader(terminal.getOutput()));
|
||||||
BufferedReader errorReader = new BufferedReader(new StringReader(terminal.getErrorOutput()))
|
BufferedReader errorReader = new BufferedReader(new StringReader(terminal.getErrorOutput()))
|
||||||
) {
|
) {
|
||||||
assertEquals("-> removing [fake]...", reader.readLine());
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"ERROR: plugin [fake] not found; run 'elasticsearch-plugin list' to get list of installed plugins",
|
"ERROR: plugin [fake] not found; run 'elasticsearch-plugin list' to get list of installed plugins",
|
||||||
errorReader.readLine()
|
errorReader.readLine()
|
||||||
|
@ -247,10 +270,14 @@ public class RemovePluginCommandTests extends ESTestCase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testMissingPluginName() throws Exception {
|
public void testMissingPluginName() {
|
||||||
UserException e = expectThrows(UserException.class, () -> removePlugin(null, home, randomBoolean()));
|
UserException e = expectThrows(UserException.class, () -> removePlugin((List<String>) null, home, randomBoolean()));
|
||||||
assertEquals(ExitCodes.USAGE, e.exitCode);
|
assertEquals(ExitCodes.USAGE, e.exitCode);
|
||||||
assertEquals("plugin name is required", e.getMessage());
|
assertEquals("At least one plugin ID is required", e.getMessage());
|
||||||
|
|
||||||
|
e = expectThrows(UserException.class, () -> removePlugin(emptyList(), home, randomBoolean()));
|
||||||
|
assertEquals(ExitCodes.USAGE, e.exitCode);
|
||||||
|
assertEquals("At least one plugin ID is required", e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testRemoveWhenRemovingMarker() throws Exception {
|
public void testRemoveWhenRemovingMarker() throws Exception {
|
||||||
|
|
|
@ -182,6 +182,16 @@ purge the configuration files while removing a plugin, use `-p` or `--purge`.
|
||||||
This can option can be used after a plugin is removed to remove any lingering
|
This can option can be used after a plugin is removed to remove any lingering
|
||||||
configuration files.
|
configuration files.
|
||||||
|
|
||||||
|
[[removing-multiple-plugins]]
|
||||||
|
=== Removing multiple plugins
|
||||||
|
|
||||||
|
Multiple plugins can be removed in one invocation as follows:
|
||||||
|
|
||||||
|
[source,shell]
|
||||||
|
-----------------------------------
|
||||||
|
sudo bin/elasticsearch-plugin remove [pluginname] [pluginname] ... [pluginname]
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
[discrete]
|
[discrete]
|
||||||
=== Updating plugins
|
=== Updating plugins
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue