Skip to content

Commit ca9ca68

Browse files
committed
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.
1 parent 16c0747 commit ca9ca68

File tree

3 files changed

+111
-31
lines changed

3 files changed

+111
-31
lines changed

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

+37-26
Original file line numberDiff line numberDiff line change
@@ -212,24 +212,50 @@ protected void printAdditionalHelp(Terminal terminal) {
212212

213213
@Override
214214
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
215-
String pluginId = arguments.value(options);
215+
List<String> pluginId = arguments.values(options);
216216
final boolean isBatch = options.has(batchOption);
217217
execute(terminal, pluginId, isBatch, env);
218218
}
219219

220220
// pkg private for testing
221-
void execute(Terminal terminal, String pluginId, boolean isBatch, Environment env) throws Exception {
222-
if (pluginId == null) {
223-
throw new UserException(ExitCodes.USAGE, "plugin id is required");
221+
void execute(Terminal terminal, List<String> pluginIds, boolean isBatch, Environment env) throws Exception {
222+
if (pluginIds.isEmpty()) {
223+
throw new UserException(ExitCodes.USAGE, "at least one plugin id is required");
224224
}
225225

226-
if ("x-pack".equals(pluginId)) {
227-
handleInstallXPack(buildFlavor());
226+
final Set<String> uniquePluginIds = new HashSet<>();
227+
for (final String pluginId : pluginIds) {
228+
if (uniquePluginIds.add(pluginId) == false) {
229+
throw new UserException(ExitCodes.USAGE, "duplicate plugin id [" + pluginId + "]");
230+
}
231+
}
232+
233+
final List<Path> deleteOnFailure = new ArrayList<>();
234+
final Set<PluginInfo> pluginInfos = new HashSet<>();
235+
for (final String pluginId : pluginIds) {
236+
try {
237+
if ("x-pack".equals(pluginId)) {
238+
handleInstallXPack(buildFlavor());
239+
}
240+
241+
final Path pluginZip = download(terminal, pluginId, env.tmpFile(), isBatch);
242+
final Path extractedZip = unzip(pluginZip, env.pluginsFile());
243+
deleteOnFailure.add(extractedZip);
244+
final PluginInfo pluginInfo = installPlugin(terminal, isBatch, extractedZip, env, deleteOnFailure);
245+
pluginInfos.add(pluginInfo);
246+
} catch (final Exception installProblem) {
247+
try {
248+
IOUtils.rm(deleteOnFailure.toArray(new Path[0]));
249+
} catch (final IOException exceptionWhileRemovingFiles) {
250+
installProblem.addSuppressed(exceptionWhileRemovingFiles);
251+
}
252+
throw installProblem;
253+
}
228254
}
229255

230-
Path pluginZip = download(terminal, pluginId, env.tmpFile(), isBatch);
231-
Path extractedZip = unzip(pluginZip, env.pluginsFile());
232-
install(terminal, isBatch, extractedZip, env);
256+
for (final PluginInfo pluginInfo : pluginInfos) {
257+
terminal.println("-> Installed " + pluginInfo.getName());
258+
}
233259
}
234260

235261
Build.Flavor buildFlavor() {
@@ -779,26 +805,11 @@ void jarHellCheck(PluginInfo candidateInfo, Path candidateDir, Path pluginsDir,
779805
// TODO: verify the classname exists in one of the jars!
780806
}
781807

782-
private void install(Terminal terminal, boolean isBatch, Path tmpRoot, Environment env) throws Exception {
783-
List<Path> deleteOnFailure = new ArrayList<>();
784-
deleteOnFailure.add(tmpRoot);
785-
try {
786-
installPlugin(terminal, isBatch, tmpRoot, env, deleteOnFailure);
787-
} catch (Exception installProblem) {
788-
try {
789-
IOUtils.rm(deleteOnFailure.toArray(new Path[0]));
790-
} catch (IOException exceptionWhileRemovingFiles) {
791-
installProblem.addSuppressed(exceptionWhileRemovingFiles);
792-
}
793-
throw installProblem;
794-
}
795-
}
796-
797808
/**
798809
* Installs the plugin from {@code tmpRoot} into the plugins dir.
799810
* If the plugin has a bin dir and/or a config dir, those are moved.
800811
*/
801-
private void installPlugin(Terminal terminal, boolean isBatch, Path tmpRoot,
812+
private PluginInfo installPlugin(Terminal terminal, boolean isBatch, Path tmpRoot,
802813
Environment env, List<Path> deleteOnFailure) throws Exception {
803814
final PluginInfo info = loadPluginInfo(terminal, tmpRoot, env);
804815
// read optional security policy (extra permissions), if it exists, confirm or warn the user
@@ -817,7 +828,7 @@ private void installPlugin(Terminal terminal, boolean isBatch, Path tmpRoot,
817828
installPluginSupportFiles(info, tmpRoot, env.binFile().resolve(info.getName()),
818829
env.configFile().resolve(info.getName()), deleteOnFailure);
819830
movePlugin(tmpRoot, destination);
820-
terminal.println("-> Installed " + info.getName());
831+
return info;
821832
}
822833

823834
/** Moves bin and config directories from the plugin if they exist */

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

+48-5
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import java.io.BufferedReader;
6565
import java.io.ByteArrayInputStream;
6666
import java.io.ByteArrayOutputStream;
67+
import java.io.FileNotFoundException;
6768
import java.io.IOException;
6869
import java.io.InputStream;
6970
import java.io.StringReader;
@@ -93,6 +94,7 @@
9394
import java.security.NoSuchProviderException;
9495
import java.util.ArrayList;
9596
import java.util.Arrays;
97+
import java.util.Collections;
9698
import java.util.Date;
9799
import java.util.HashSet;
98100
import java.util.List;
@@ -280,9 +282,17 @@ void installPlugin(String pluginUrl, Path home) throws Exception {
280282
installPlugin(pluginUrl, home, skipJarHellCommand);
281283
}
282284

285+
void installPlugins(final List<String> pluginUrls, final Path home) throws Exception {
286+
installPlugins(pluginUrls, home, skipJarHellCommand);
287+
}
288+
283289
void installPlugin(String pluginUrl, Path home, InstallPluginCommand command) throws Exception {
284-
Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", home).build());
285-
command.execute(terminal, pluginUrl, false, env);
290+
installPlugins(pluginUrl == null ? Collections.emptyList() : Collections.singletonList(pluginUrl), home, command);
291+
}
292+
293+
void installPlugins(final List<String> pluginUrls, final Path home, final InstallPluginCommand command) throws Exception {
294+
final Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", home).build());
295+
command.execute(terminal, pluginUrls, false, env);
286296
}
287297

288298
void assertPlugin(String name, Path original, Environment env) throws IOException {
@@ -382,7 +392,7 @@ void assertInstallCleaned(Environment env) throws IOException {
382392
public void testMissingPluginId() throws IOException {
383393
final Tuple<Path, Environment> env = createEnv(fs, temp);
384394
final UserException e = expectThrows(UserException.class, () -> installPlugin(null, env.v1()));
385-
assertTrue(e.getMessage(), e.getMessage().contains("plugin id is required"));
395+
assertTrue(e.getMessage(), e.getMessage().contains("at least one plugin id is required"));
386396
}
387397

388398
public void testSomethingWorks() throws Exception {
@@ -393,6 +403,38 @@ public void testSomethingWorks() throws Exception {
393403
assertPlugin("fake", pluginDir, env.v2());
394404
}
395405

406+
public void testMultipleWorks() throws Exception {
407+
Tuple<Path, Environment> env = createEnv(fs, temp);
408+
Path pluginDir = createPluginDir(temp);
409+
String fake1PluginZip = createPluginUrl("fake1", pluginDir);
410+
String fake2PluginZip = createPluginUrl("fake2", pluginDir);
411+
installPlugins(Arrays.asList(fake1PluginZip, fake2PluginZip), env.v1());
412+
assertPlugin("fake1", pluginDir, env.v2());
413+
assertPlugin("fake2", pluginDir, env.v2());
414+
}
415+
416+
public void testDuplicateInstall() throws Exception {
417+
Tuple<Path, Environment> env = createEnv(fs, temp);
418+
Path pluginDir = createPluginDir(temp);
419+
String pluginZip = createPluginUrl("fake", pluginDir);
420+
final UserException e = expectThrows(UserException.class, () -> installPlugins(Arrays.asList(pluginZip, pluginZip), env.v1()));
421+
assertThat(e, hasToString(containsString("duplicate plugin id [" + pluginZip + "]")));
422+
}
423+
424+
public void testTransaction() throws Exception {
425+
Tuple<Path, Environment> env = createEnv(fs, temp);
426+
Path pluginDir = createPluginDir(temp);
427+
String pluginZip = createPluginUrl("fake", pluginDir);
428+
final FileNotFoundException e = expectThrows(
429+
FileNotFoundException.class,
430+
() -> installPlugins(Arrays.asList(pluginZip, pluginZip + "does-not-exist"), env.v1()));
431+
assertThat(e, hasToString(containsString("does-not-exist")));
432+
final Path fakeInstallPath = env.v2().pluginsFile().resolve("fake");
433+
// fake should have been removed when the file not found exception occurred
434+
assertFalse(Files.exists(fakeInstallPath));
435+
assertInstallCleaned(env.v2());
436+
}
437+
396438
public void testInstallFailsIfPreviouslyRemovedPluginFailed() throws Exception {
397439
Tuple<Path, Environment> env = createEnv(fs, temp);
398440
Path pluginDir = createPluginDir(temp);
@@ -769,7 +811,8 @@ Build.Flavor buildFlavor() {
769811
};
770812

771813
final Environment environment = createEnv(fs, temp).v2();
772-
final T exception = expectThrows(clazz, () -> flavorCommand.execute(terminal, "x-pack", false, environment));
814+
final T exception =
815+
expectThrows(clazz, () -> flavorCommand.execute(terminal, Collections.singletonList("x-pack"), false, environment));
773816
assertThat(exception, hasToString(containsString(expectedMessage)));
774817
}
775818

@@ -830,7 +873,7 @@ private void installPlugin(MockTerminal terminal, boolean isBatch) throws Except
830873
writePluginSecurityPolicy(pluginDir, "setFactory");
831874
}
832875
String pluginZip = createPlugin("fake", pluginDir).toUri().toURL().toString();
833-
skipJarHellCommand.execute(terminal, pluginZip, isBatch, env.v2());
876+
skipJarHellCommand.execute(terminal, Collections.singletonList(pluginZip), isBatch, env.v2());
834877
}
835878

836879
void assertInstallPluginFromUrl(

docs/plugins/plugin-script.asciidoc

+26
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,32 @@ sudo ES_JAVA_OPTS="-Djavax.net.ssl.trustStore=/path/to/trustStore.jks" bin/elast
106106
-----------------------------------
107107
--
108108

109+
[[installing-multiple-plugins]]
110+
=== Installing multiple plugins
111+
112+
Multiple plugins can be installed in one invocation as follows:
113+
114+
[source,shell]
115+
-----------------------------------
116+
sudo bin/elasticsearch-plugin install [plugin_id] [plugin_id] ... [plugin_id]
117+
-----------------------------------
118+
119+
Each `plugin_id` can be any valid form for installing a single plugin (e.g., the
120+
name of a core plugin, or a custom URL).
121+
122+
For instance, to install the core <<analysis-icu,ICU plugin>>, and
123+
<<repository-s3,S3 repository plugin>> run the following command:
124+
125+
[source,shell]
126+
-----------------------------------
127+
sudo bin/elasticsearch-plugin install analysis-icu repository-s3
128+
-----------------------------------
129+
130+
This command will install the versions of the plugins that matches your
131+
Elasticsearch version. The installation will be treated as a transaction, so
132+
that all the plugins will be installed, or none of the plugins will be installed
133+
if any installation fails.
134+
109135
[[mandatory-plugins]]
110136
=== Mandatory Plugins
111137

0 commit comments

Comments
 (0)