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.
This commit is contained in:
Jason Tedor 2020-01-14 12:12:20 -05:00
parent 16c07472e5
commit ca9ca68cbe
No known key found for this signature in database
GPG Key ID: 8CF9C19984731E85
3 changed files with 111 additions and 31 deletions

View File

@ -212,24 +212,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 + "]");
}
}
Path pluginZip = download(terminal, pluginId, env.tmpFile(), isBatch);
Path extractedZip = unzip(pluginZip, env.pluginsFile());
install(terminal, isBatch, extractedZip, env);
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;
}
}
for (final PluginInfo pluginInfo : pluginInfos) {
terminal.println("-> Installed " + pluginInfo.getName());
}
}
Build.Flavor buildFlavor() {
@ -779,26 +805,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
@ -817,7 +828,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 */

View File

@ -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;
@ -93,6 +94,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
@ -280,9 +282,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 ? Collections.emptyList() : Collections.singletonList(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 +392,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 +403,38 @@ 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(Arrays.asList(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(Arrays.asList(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(Arrays.asList(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 +811,8 @@ 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, Collections.singletonList("x-pack"), false, environment));
assertThat(exception, hasToString(containsString(expectedMessage)));
}
@ -830,7 +873,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, Collections.singletonList(pluginZip), isBatch, env.v2());
}
void assertInstallPluginFromUrl(

View File

@ -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