From 610c4ee9464467d870985144c255d25cd3fc52ac Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Tue, 14 Jan 2020 12:12:20 -0500 Subject: [PATCH] Allow installing multiple plugins as a transaction (#50924) This commit allows the plugin installer to install multiple plugins in a single invocation. The installation will be treated as a transaction, so that all of the plugins are install successfully, or none of the plugins are installed. --- .../plugins/InstallPluginCommand.java | 63 +++++++++++-------- .../plugins/InstallPluginCommandTests.java | 50 +++++++++++++-- docs/plugins/plugin-script.asciidoc | 26 ++++++++ 3 files changed, 108 insertions(+), 31 deletions(-) diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java index f404f941168d7..9205c248e519e 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java @@ -206,24 +206,50 @@ protected void printAdditionalHelp(Terminal terminal) { @Override protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception { - String pluginId = arguments.value(options); + List pluginId = arguments.values(options); final boolean isBatch = options.has(batchOption); execute(terminal, pluginId, isBatch, env); } // pkg private for testing - void execute(Terminal terminal, String pluginId, boolean isBatch, Environment env) throws Exception { - if (pluginId == null) { - throw new UserException(ExitCodes.USAGE, "plugin id is required"); + void execute(Terminal terminal, List pluginIds, boolean isBatch, Environment env) throws Exception { + if (pluginIds.isEmpty()) { + throw new UserException(ExitCodes.USAGE, "at least one plugin id is required"); } - if ("x-pack".equals(pluginId)) { - handleInstallXPack(buildFlavor()); + final Set uniquePluginIds = new HashSet<>(); + for (final String pluginId : pluginIds) { + if (uniquePluginIds.add(pluginId) == false) { + throw new UserException(ExitCodes.USAGE, "duplicate plugin id [" + pluginId + "]"); + } + } + + final List deleteOnFailure = new ArrayList<>(); + final Set pluginInfos = new HashSet<>(); + for (final String pluginId : pluginIds) { + try { + if ("x-pack".equals(pluginId)) { + handleInstallXPack(buildFlavor()); + } + + final Path pluginZip = download(terminal, pluginId, env.tmpFile(), isBatch); + final Path extractedZip = unzip(pluginZip, env.pluginsFile()); + deleteOnFailure.add(extractedZip); + final PluginInfo pluginInfo = installPlugin(terminal, isBatch, extractedZip, env, deleteOnFailure); + pluginInfos.add(pluginInfo); + } catch (final Exception installProblem) { + try { + IOUtils.rm(deleteOnFailure.toArray(new Path[0])); + } catch (final IOException exceptionWhileRemovingFiles) { + installProblem.addSuppressed(exceptionWhileRemovingFiles); + } + throw installProblem; + } } - Path pluginZip = download(terminal, pluginId, env.tmpFile(), isBatch); - Path extractedZip = unzip(pluginZip, env.pluginsFile()); - install(terminal, isBatch, extractedZip, env); + for (final PluginInfo pluginInfo : pluginInfos) { + terminal.println("-> Installed " + pluginInfo.getName()); + } } Build.Flavor buildFlavor() { @@ -773,26 +799,11 @@ void jarHellCheck(PluginInfo candidateInfo, Path candidateDir, Path pluginsDir, // TODO: verify the classname exists in one of the jars! } - private void install(Terminal terminal, boolean isBatch, Path tmpRoot, Environment env) throws Exception { - List deleteOnFailure = new ArrayList<>(); - deleteOnFailure.add(tmpRoot); - try { - installPlugin(terminal, isBatch, tmpRoot, env, deleteOnFailure); - } catch (Exception installProblem) { - try { - IOUtils.rm(deleteOnFailure.toArray(new Path[0])); - } catch (IOException exceptionWhileRemovingFiles) { - installProblem.addSuppressed(exceptionWhileRemovingFiles); - } - throw installProblem; - } - } - /** * Installs the plugin from {@code tmpRoot} into the plugins dir. * If the plugin has a bin dir and/or a config dir, those are moved. */ - private void installPlugin(Terminal terminal, boolean isBatch, Path tmpRoot, + private PluginInfo installPlugin(Terminal terminal, boolean isBatch, Path tmpRoot, Environment env, List deleteOnFailure) throws Exception { final PluginInfo info = loadPluginInfo(terminal, tmpRoot, env); // read optional security policy (extra permissions), if it exists, confirm or warn the user @@ -811,7 +822,7 @@ private void installPlugin(Terminal terminal, boolean isBatch, Path tmpRoot, installPluginSupportFiles(info, tmpRoot, env.binFile().resolve(info.getName()), env.configFile().resolve(info.getName()), deleteOnFailure); movePlugin(tmpRoot, destination); - terminal.println("-> Installed " + info.getName()); + return info; } /** Moves bin and config directories from the plugin if they exist */ diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java index 45fbd3133d175..6822f07d4c10f 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java @@ -64,6 +64,7 @@ import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; @@ -280,9 +281,17 @@ void installPlugin(String pluginUrl, Path home) throws Exception { installPlugin(pluginUrl, home, skipJarHellCommand); } + void installPlugins(final List pluginUrls, final Path home) throws Exception { + installPlugins(pluginUrls, home, skipJarHellCommand); + } + void installPlugin(String pluginUrl, Path home, InstallPluginCommand command) throws Exception { - Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", home).build()); - command.execute(terminal, pluginUrl, false, env); + installPlugins(pluginUrl == null ? List.of() : List.of(pluginUrl), home, command); + } + + void installPlugins(final List pluginUrls, final Path home, final InstallPluginCommand command) throws Exception { + final Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", home).build()); + command.execute(terminal, pluginUrls, false, env); } void assertPlugin(String name, Path original, Environment env) throws IOException { @@ -382,7 +391,7 @@ void assertInstallCleaned(Environment env) throws IOException { public void testMissingPluginId() throws IOException { final Tuple env = createEnv(fs, temp); final UserException e = expectThrows(UserException.class, () -> installPlugin(null, env.v1())); - assertTrue(e.getMessage(), e.getMessage().contains("plugin id is required")); + assertTrue(e.getMessage(), e.getMessage().contains("at least one plugin id is required")); } public void testSomethingWorks() throws Exception { @@ -393,6 +402,37 @@ public void testSomethingWorks() throws Exception { assertPlugin("fake", pluginDir, env.v2()); } + public void testMultipleWorks() throws Exception { + Tuple env = createEnv(fs, temp); + Path pluginDir = createPluginDir(temp); + String fake1PluginZip = createPluginUrl("fake1", pluginDir); + String fake2PluginZip = createPluginUrl("fake2", pluginDir); + installPlugins(List.of(fake1PluginZip, fake2PluginZip), env.v1()); + assertPlugin("fake1", pluginDir, env.v2()); + assertPlugin("fake2", pluginDir, env.v2()); + } + + public void testDuplicateInstall() throws Exception { + Tuple env = createEnv(fs, temp); + Path pluginDir = createPluginDir(temp); + String pluginZip = createPluginUrl("fake", pluginDir); + final UserException e = expectThrows(UserException.class, () -> installPlugins(List.of(pluginZip, pluginZip), env.v1())); + assertThat(e, hasToString(containsString("duplicate plugin id [" + pluginZip + "]"))); + } + + public void testTransaction() throws Exception { + Tuple env = createEnv(fs, temp); + Path pluginDir = createPluginDir(temp); + String pluginZip = createPluginUrl("fake", pluginDir); + final FileNotFoundException e = + expectThrows(FileNotFoundException.class, () -> installPlugins(List.of(pluginZip, pluginZip + "does-not-exist"), env.v1())); + assertThat(e, hasToString(containsString("does-not-exist"))); + final Path fakeInstallPath = env.v2().pluginsFile().resolve("fake"); + // fake should have been removed when the file not found exception occurred + assertFalse(Files.exists(fakeInstallPath)); + assertInstallCleaned(env.v2()); + } + public void testInstallFailsIfPreviouslyRemovedPluginFailed() throws Exception { Tuple env = createEnv(fs, temp); Path pluginDir = createPluginDir(temp); @@ -769,7 +809,7 @@ Build.Flavor buildFlavor() { }; final Environment environment = createEnv(fs, temp).v2(); - final T exception = expectThrows(clazz, () -> flavorCommand.execute(terminal, "x-pack", false, environment)); + final T exception = expectThrows(clazz, () -> flavorCommand.execute(terminal, List.of("x-pack"), false, environment)); assertThat(exception, hasToString(containsString(expectedMessage))); } @@ -830,7 +870,7 @@ private void installPlugin(MockTerminal terminal, boolean isBatch) throws Except writePluginSecurityPolicy(pluginDir, "setFactory"); } String pluginZip = createPlugin("fake", pluginDir).toUri().toURL().toString(); - skipJarHellCommand.execute(terminal, pluginZip, isBatch, env.v2()); + skipJarHellCommand.execute(terminal, List.of(pluginZip), isBatch, env.v2()); } void assertInstallPluginFromUrl( diff --git a/docs/plugins/plugin-script.asciidoc b/docs/plugins/plugin-script.asciidoc index f7906a0be50ec..37bc8b6e66557 100644 --- a/docs/plugins/plugin-script.asciidoc +++ b/docs/plugins/plugin-script.asciidoc @@ -106,6 +106,32 @@ sudo ES_JAVA_OPTS="-Djavax.net.ssl.trustStore=/path/to/trustStore.jks" bin/elast ----------------------------------- -- +[[installing-multiple-plugins]] +=== Installing multiple plugins + +Multiple plugins can be installed in one invocation as follows: + +[source,shell] +----------------------------------- +sudo bin/elasticsearch-plugin install [plugin_id] [plugin_id] ... [plugin_id] +----------------------------------- + +Each `plugin_id` can be any valid form for installing a single plugin (e.g., the +name of a core plugin, or a custom URL). + +For instance, to install the core <>, and +<> run the following command: + +[source,shell] +----------------------------------- +sudo bin/elasticsearch-plugin install analysis-icu repository-s3 +----------------------------------- + +This command will install the versions of the plugins that matches your +Elasticsearch version. The installation will be treated as a transaction, so +that all the plugins will be installed, or none of the plugins will be installed +if any installation fails. + [[mandatory-plugins]] === Mandatory Plugins