瀏覽代碼

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.
Jason Tedor 5 年之前
父節點
當前提交
6b20a2c176

+ 37 - 26
distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java

@@ -206,24 +206,50 @@ class InstallPluginCommand extends EnvironmentAwareCommand {
 
     @Override
     protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
-        String pluginId = arguments.value(options);
+        List<String> 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<String> 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<String> uniquePluginIds = new HashSet<>();
+        for (final String pluginId : pluginIds) {
+            if (uniquePluginIds.add(pluginId) == false) {
+                throw new UserException(ExitCodes.USAGE, "duplicate plugin id [" + pluginId + "]");
+            }
+        }
+
+        final List<Path> deleteOnFailure = new ArrayList<>();
+        final Set<PluginInfo> 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 @@ class InstallPluginCommand extends EnvironmentAwareCommand {
         // TODO: verify the classname exists in one of the jars!
     }
 
-    private void install(Terminal terminal, boolean isBatch, Path tmpRoot, Environment env) throws Exception {
-        List<Path> 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<Path> 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 @@ class InstallPluginCommand extends EnvironmentAwareCommand {
         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 */

+ 45 - 5
distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java

@@ -64,6 +64,7 @@ import org.junit.Before;
 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 @@ public class InstallPluginCommandTests extends ESTestCase {
         installPlugin(pluginUrl, home, skipJarHellCommand);
     }
 
+    void installPlugins(final List<String> 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<String> 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 @@ public class InstallPluginCommandTests extends ESTestCase {
     public void testMissingPluginId() throws IOException {
         final Tuple<Path, Environment> 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 class InstallPluginCommandTests extends ESTestCase {
         assertPlugin("fake", pluginDir, env.v2());
     }
 
+    public void testMultipleWorks() throws Exception {
+        Tuple<Path, Environment> 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<Path, Environment> 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<Path, Environment> 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<Path, Environment> env = createEnv(fs, temp);
         Path pluginDir = createPluginDir(temp);
@@ -769,7 +809,7 @@ public class InstallPluginCommandTests extends ESTestCase {
         };
 
         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 @@ public class InstallPluginCommandTests extends ESTestCase {
             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(

+ 26 - 0
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 <<analysis-icu,ICU plugin>>, and
+<<repository-s3,S3 repository plugin>> 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