Skip to content

Commit 610c4ee

Browse files
jasontedorSivagurunathanV
authored andcommitted
Allow installing multiple plugins as a transaction (elastic#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 da6ed28 commit 610c4ee

File tree

3 files changed

+108
-31
lines changed

3 files changed

+108
-31
lines changed

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

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -206,24 +206,50 @@ protected void printAdditionalHelp(Terminal terminal) {
206206

207207
@Override
208208
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
209-
String pluginId = arguments.value(options);
209+
List<String> pluginId = arguments.values(options);
210210
final boolean isBatch = options.has(batchOption);
211211
execute(terminal, pluginId, isBatch, env);
212212
}
213213

214214
// pkg private for testing
215-
void execute(Terminal terminal, String pluginId, boolean isBatch, Environment env) throws Exception {
216-
if (pluginId == null) {
217-
throw new UserException(ExitCodes.USAGE, "plugin id is required");
215+
void execute(Terminal terminal, List<String> pluginIds, boolean isBatch, Environment env) throws Exception {
216+
if (pluginIds.isEmpty()) {
217+
throw new UserException(ExitCodes.USAGE, "at least one plugin id is required");
218218
}
219219

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

224-
Path pluginZip = download(terminal, pluginId, env.tmpFile(), isBatch);
225-
Path extractedZip = unzip(pluginZip, env.pluginsFile());
226-
install(terminal, isBatch, extractedZip, env);
250+
for (final PluginInfo pluginInfo : pluginInfos) {
251+
terminal.println("-> Installed " + pluginInfo.getName());
252+
}
227253
}
228254

229255
Build.Flavor buildFlavor() {
@@ -773,26 +799,11 @@ void jarHellCheck(PluginInfo candidateInfo, Path candidateDir, Path pluginsDir,
773799
// TODO: verify the classname exists in one of the jars!
774800
}
775801

776-
private void install(Terminal terminal, boolean isBatch, Path tmpRoot, Environment env) throws Exception {
777-
List<Path> deleteOnFailure = new ArrayList<>();
778-
deleteOnFailure.add(tmpRoot);
779-
try {
780-
installPlugin(terminal, isBatch, tmpRoot, env, deleteOnFailure);
781-
} catch (Exception installProblem) {
782-
try {
783-
IOUtils.rm(deleteOnFailure.toArray(new Path[0]));
784-
} catch (IOException exceptionWhileRemovingFiles) {
785-
installProblem.addSuppressed(exceptionWhileRemovingFiles);
786-
}
787-
throw installProblem;
788-
}
789-
}
790-
791802
/**
792803
* Installs the plugin from {@code tmpRoot} into the plugins dir.
793804
* If the plugin has a bin dir and/or a config dir, those are moved.
794805
*/
795-
private void installPlugin(Terminal terminal, boolean isBatch, Path tmpRoot,
806+
private PluginInfo installPlugin(Terminal terminal, boolean isBatch, Path tmpRoot,
796807
Environment env, List<Path> deleteOnFailure) throws Exception {
797808
final PluginInfo info = loadPluginInfo(terminal, tmpRoot, env);
798809
// 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,
811822
installPluginSupportFiles(info, tmpRoot, env.binFile().resolve(info.getName()),
812823
env.configFile().resolve(info.getName()), deleteOnFailure);
813824
movePlugin(tmpRoot, destination);
814-
terminal.println("-> Installed " + info.getName());
825+
return info;
815826
}
816827

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

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

Lines changed: 45 additions & 5 deletions
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;
@@ -280,9 +281,17 @@ void installPlugin(String pluginUrl, Path home) throws Exception {
280281
installPlugin(pluginUrl, home, skipJarHellCommand);
281282
}
282283

284+
void installPlugins(final List<String> pluginUrls, final Path home) throws Exception {
285+
installPlugins(pluginUrls, home, skipJarHellCommand);
286+
}
287+
283288
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);
289+
installPlugins(pluginUrl == null ? List.of() : List.of(pluginUrl), home, command);
290+
}
291+
292+
void installPlugins(final List<String> pluginUrls, final Path home, final InstallPluginCommand command) throws Exception {
293+
final Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", home).build());
294+
command.execute(terminal, pluginUrls, false, env);
286295
}
287296

288297
void assertPlugin(String name, Path original, Environment env) throws IOException {
@@ -382,7 +391,7 @@ void assertInstallCleaned(Environment env) throws IOException {
382391
public void testMissingPluginId() throws IOException {
383392
final Tuple<Path, Environment> env = createEnv(fs, temp);
384393
final UserException e = expectThrows(UserException.class, () -> installPlugin(null, env.v1()));
385-
assertTrue(e.getMessage(), e.getMessage().contains("plugin id is required"));
394+
assertTrue(e.getMessage(), e.getMessage().contains("at least one plugin id is required"));
386395
}
387396

388397
public void testSomethingWorks() throws Exception {
@@ -393,6 +402,37 @@ public void testSomethingWorks() throws Exception {
393402
assertPlugin("fake", pluginDir, env.v2());
394403
}
395404

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

771811
final Environment environment = createEnv(fs, temp).v2();
772-
final T exception = expectThrows(clazz, () -> flavorCommand.execute(terminal, "x-pack", false, environment));
812+
final T exception = expectThrows(clazz, () -> flavorCommand.execute(terminal, List.of("x-pack"), false, environment));
773813
assertThat(exception, hasToString(containsString(expectedMessage)));
774814
}
775815

@@ -830,7 +870,7 @@ private void installPlugin(MockTerminal terminal, boolean isBatch) throws Except
830870
writePluginSecurityPolicy(pluginDir, "setFactory");
831871
}
832872
String pluginZip = createPlugin("fake", pluginDir).toUri().toURL().toString();
833-
skipJarHellCommand.execute(terminal, pluginZip, isBatch, env.v2());
873+
skipJarHellCommand.execute(terminal, List.of(pluginZip), isBatch, env.v2());
834874
}
835875

836876
void assertInstallPluginFromUrl(

docs/plugins/plugin-script.asciidoc

Lines changed: 26 additions & 0 deletions
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)