Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce declarative plugin management #77544

Merged
merged 117 commits into from
Nov 15, 2021
Merged
Changes from 1 commit
Commits
Show all changes
117 commits
Select commit Hold shift + click to select a range
6eda87c
Proof of concept for declarative plugin management
pugnascotia Jun 8, 2021
a57e8f4
More work
pugnascotia Jun 24, 2021
c45199e
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Aug 3, 2021
264012f
WIP
pugnascotia Aug 5, 2021
f1e01e0
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Aug 26, 2021
1a61bb5
Get basic sync working again, and fix existing tests
pugnascotia Aug 31, 2021
08ed84e
Progress
pugnascotia Sep 2, 2021
89b1f19
Tests
pugnascotia Sep 3, 2021
5db7f0b
More tests
pugnascotia Sep 3, 2021
8174d07
More tests
pugnascotia Sep 5, 2021
fc53fbb
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 6, 2021
4b53a04
Sync command can use the plugin archive
pugnascotia Sep 6, 2021
b541732
Remove plugin wrapper tool since it's redundant
pugnascotia Sep 6, 2021
4395902
Test plugins sync in Docker cloud-ess
pugnascotia Sep 7, 2021
91c1ce2
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 7, 2021
e66ab4f
Rename default plugins config to deactivate it by default
pugnascotia Sep 7, 2021
534e434
Tweaks
pugnascotia Sep 7, 2021
3a6e4df
Remove call to plugins sync from Windows startup file
pugnascotia Sep 7, 2021
11f2004
Tweaks and Javadoc
pugnascotia Sep 7, 2021
816bae1
Strip out per-plugin proxies, fix checks
pugnascotia Sep 7, 2021
8122c1e
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 8, 2021
40b7033
Simplify proxy code
pugnascotia Sep 8, 2021
db7af8f
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 8, 2021
612979b
Simplify proxy configuration
pugnascotia Sep 9, 2021
979d0a3
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 9, 2021
f0a5fbd
Forid plugin ID differing in the downloaded properties
pugnascotia Sep 9, 2021
05a350f
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 10, 2021
9829056
Add example plugins config as configuration file in rpm / deb
pugnascotia Sep 10, 2021
1e1ffc7
Tweak checkstyle comment
pugnascotia Sep 14, 2021
8df977c
Rename url field to location
pugnascotia Sep 14, 2021
7d1d210
Change prefix of plugin archive dir env var
pugnascotia Sep 14, 2021
91af84e
Extract constant
pugnascotia Sep 14, 2021
c735a0a
Rename PluginsManifest to PluginsConfig
pugnascotia Sep 14, 2021
94c15e9
Swap URI for URL
pugnascotia Sep 14, 2021
6e1a68b
Include exception as cause when parsing the config
pugnascotia Sep 14, 2021
7282c7d
URI tweak
pugnascotia Sep 14, 2021
524033a
Make the Docker layer cache usable for snapshot builds
pugnascotia Sep 14, 2021
8337588
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 14, 2021
ffbb4d3
Begin moving plugin sync code to bootstrap process
pugnascotia Sep 16, 2021
a779dfa
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 21, 2021
606a138
Rearrange code
pugnascotia Sep 21, 2021
cbed833
Tweak ES policy for jackson-databind
pugnascotia Sep 21, 2021
a35dbf6
Use xcontent for parsing instead of jackson
pugnascotia Sep 23, 2021
614be7c
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 23, 2021
83192eb
Fix Docker again
pugnascotia Sep 23, 2021
8f822b0
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 23, 2021
a09c13f
WIP
pugnascotia Sep 23, 2021
1655aa3
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 23, 2021
71efa11
Disable BouncyCastle in server until I figure out the break
pugnascotia Sep 23, 2021
7c222f5
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 24, 2021
a67aa8b
Restore bc fips jars in server
pugnascotia Sep 24, 2021
0328678
Tweaks
pugnascotia Sep 24, 2021
13433ab
Refactoring
pugnascotia Sep 24, 2021
cbba285
Only use PluginsManager with Docker distributions
pugnascotia Sep 24, 2021
77f3787
Update :x-pack:plugin:identity-provider:thirdPartyAudit exclusions
pugnascotia Sep 24, 2021
22028bf
WIP to load plugin-cli jars
pugnascotia Sep 28, 2021
8abde31
Abstract a logging interface for plugin actions
pugnascotia Sep 28, 2021
7fd932d
Abstract a logging interface for plugin actions
pugnascotia Sep 28, 2021
d77e645
Use plugin-cli classes via a classloader
pugnascotia Sep 28, 2021
5ee9d71
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 28, 2021
b897a58
Revert some changes
pugnascotia Sep 28, 2021
7b6325b
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Sep 28, 2021
8c2fa96
Fixes
pugnascotia Sep 28, 2021
494efae
Refactor RemovePluginAction to be more like a library
pugnascotia Sep 29, 2021
0319a20
Move plugin action classes to a sub-package
pugnascotia Sep 29, 2021
db9e0a8
Revert logging abstraction
pugnascotia Sep 30, 2021
a6f51b1
Merge remote-tracking branch 'upstream/master' into 70219-refactor-pl…
pugnascotia Sep 30, 2021
87bd95e
Tweaks
pugnascotia Sep 30, 2021
b9d78ac
Move exit code logic for installing plugins to the command class
pugnascotia Sep 30, 2021
7432e46
Merge branch '70219-refactor-plugin-actions-further' into 70219-decla…
pugnascotia Sep 30, 2021
4e09190
Fixes after merging
pugnascotia Sep 30, 2021
9ceb944
Tidy up plugin install logging in headless mode
pugnascotia Sep 30, 2021
ea6d5e6
Tweaks
pugnascotia Sep 30, 2021
ba73e5f
Spotless
pugnascotia Sep 30, 2021
e84d0a1
Revert to previous exceptions scheme
pugnascotia Oct 1, 2021
670227d
Move PluginSecurity to action subpackage
pugnascotia Oct 1, 2021
eb80ce2
Merge branch '70219-refactor-plugin-actions-further' into 70219-decla…
pugnascotia Oct 1, 2021
cd90a19
Move sync code to plugins cli action package
pugnascotia Oct 1, 2021
bc2358e
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Oct 1, 2021
b6bbeba
Moving code around
pugnascotia Oct 1, 2021
c48c712
Tweaks and fixes
pugnascotia Oct 1, 2021
9770a55
Cleanups
pugnascotia Oct 1, 2021
0ec9923
Fix forbidden API
pugnascotia Oct 1, 2021
ea3aaa8
Fix license header
pugnascotia Oct 1, 2021
e5dc33b
Move files to the correct new place
pugnascotia Oct 6, 2021
f051247
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Oct 7, 2021
ddbd26b
Fix tests
pugnascotia Oct 7, 2021
897a54f
Tweaks
pugnascotia Oct 7, 2021
215463e
Improve plugin CLI qa tests
pugnascotia Oct 7, 2021
3aabee8
Add test for syncing plugins via configured proxy
pugnascotia Oct 8, 2021
367cac1
Refactor
pugnascotia Oct 8, 2021
0d65a30
Don't use curl, and also test system proxy settings
pugnascotia Oct 8, 2021
d39682d
Cleanups and docs
pugnascotia Oct 8, 2021
9aa6b6f
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Oct 11, 2021
08174bc
Add unit testing
pugnascotia Oct 11, 2021
8c65b1d
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Oct 13, 2021
8dd64da
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Oct 14, 2021
3b6c077
Formatting
pugnascotia Oct 14, 2021
fb3ac3f
Test fix
pugnascotia Oct 14, 2021
673b6ea
Reformat everything before merge
pugnascotia Nov 8, 2021
bfdad33
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Nov 8, 2021
dfd6ed5
Update example plugins config
pugnascotia Nov 8, 2021
42e4110
Move plugin actions back to the cli sub-package
pugnascotia Nov 9, 2021
ef7c378
Use Build.CURRENT instead of referencing the sys prop directly
pugnascotia Nov 9, 2021
c8f5c5e
Rename SyncPluginsProvider to PluginsSynchronizer
pugnascotia Nov 9, 2021
45e5274
Move plugins config cache to plugins dir
pugnascotia Nov 9, 2021
bdaa359
Write cached config in CBOR, not YAML
pugnascotia Nov 9, 2021
435fe2d
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Nov 9, 2021
9ca2427
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Nov 10, 2021
df52770
Test Docker + plugins by including analysis-icu
pugnascotia Nov 11, 2021
6f3df48
Try re-enabling PluginCliTests
pugnascotia Nov 11, 2021
de516de
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Nov 11, 2021
d46f0bc
Fix test ordering
pugnascotia Nov 11, 2021
8f8c4a1
Try to fix dependencies
pugnascotia Nov 11, 2021
78f6e5b
Another tweak
pugnascotia Nov 11, 2021
2451c52
Merge remote-tracking branch 'upstream/master' into 70219-declarative…
pugnascotia Nov 11, 2021
6eea9a1
Disable PluginCliTests again
pugnascotia Nov 11, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Refactor RemovePluginAction to be more like a library
pugnascotia committed Sep 29, 2021
commit 494efaec0665f857363409a146f04e607900b05a
Original file line number Diff line number Diff line change
@@ -37,8 +37,8 @@
import org.elasticsearch.env.Environment;
import org.elasticsearch.jdk.JarHell;
import org.elasticsearch.plugins.Platforms;
import org.elasticsearch.plugins.PluginLogger;
import org.elasticsearch.plugins.PluginInfo;
import org.elasticsearch.plugins.PluginLogger;
import org.elasticsearch.plugins.PluginsService;

import java.io.BufferedReader;
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@

import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.core.internal.io.IOUtils;
import org.elasticsearch.env.Environment;
import org.elasticsearch.plugins.PluginLogger;
@@ -20,12 +21,10 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@@ -34,14 +33,16 @@
*/
class RemovePluginAction {

// exit codes for remove
/** A plugin cannot be removed because it is extended by another plugin. */
static final int PLUGIN_STILL_USED = 11;

private final PluginLogger logger;
private final Environment env;
private final boolean purge;

public enum RemovePluginProblem {
NOT_FOUND,
STILL_USED,
BIN_FILE_NOT_DIRECTORY
}

/**
* Creates a new action.
*
@@ -56,59 +57,46 @@ class RemovePluginAction {
}

/**
* Remove the plugin specified by {@code pluginName}.
*
* @param plugins the IDs of the plugins to remove
* @throws IOException if any I/O exception occurs while performing a file operation
* @throws UserException if plugins is null or empty
* @throws UserException if plugin directory does not exist
* @throws UserException if the plugin bin directory is not a directory
* Looks for problems that would prevent the specified plugins from being removed.
* @param plugins the plugins to check
* @return {@code null} if there are no problems, or a {@link Tuple} that indicates the type of problem,
* and a descriptive message.
* @throws IOException if a problem occurs loading the plugins that are currently installed.
*/
void execute(List<PluginDescriptor> plugins) throws IOException, UserException {
if (plugins == null || plugins.isEmpty()) {
throw new UserException(ExitCodes.USAGE, "At least one plugin ID is required");
}

ensurePluginsNotUsedByOtherPlugins(plugins);
public Tuple<RemovePluginProblem, String> checkRemovePlugins(List<PluginDescriptor> plugins) throws IOException {
final Set<PluginsService.Bundle> bundles = PluginsService.getPluginBundles(this.env.pluginsFile());

for (PluginDescriptor plugin : plugins) {
checkCanRemove(plugin);
final List<String> usedBy = checkUsedByOtherPlugins(bundles, plugin);

if (usedBy.isEmpty() == false) {
final StringBuilder message = new StringBuilder().append("cannot remove plugin [")
.append(plugin.getId())
.append(" because it is extended by other plugins:\n");
usedBy.forEach(each -> message.append("\t- ").append(each).append("\n"));
return Tuple.tuple(RemovePluginProblem.STILL_USED, message.toString());
}
}

for (PluginDescriptor plugin : plugins) {
removePlugin(plugin);
}
return plugins.stream().map(this::canRemovePlugin).filter(Objects::nonNull).findFirst().orElse(null);
}

private void ensurePluginsNotUsedByOtherPlugins(List<PluginDescriptor> plugins) throws IOException, UserException {
// First make sure nothing extends this plugin
final Map<String, List<String>> usedBy = new HashMap<>();
Set<PluginsService.Bundle> bundles = PluginsService.getPluginBundles(env.pluginsFile());
private List<String> checkUsedByOtherPlugins(Set<PluginsService.Bundle> bundles, PluginDescriptor plugin) {
final List<String> usedBy = new ArrayList<>();

for (PluginsService.Bundle bundle : bundles) {
for (String extendedPlugin : bundle.plugin.getExtendedPlugins()) {
for (PluginDescriptor plugin : plugins) {
String pluginId = plugin.getId();
if (extendedPlugin.equals(pluginId)) {
usedBy.computeIfAbsent(bundle.plugin.getName(), (_key -> new ArrayList<>())).add(pluginId);
}
String pluginId = plugin.getId();
if (extendedPlugin.equals(pluginId)) {
usedBy.add(pluginId);
}
}
}
if (usedBy.isEmpty()) {
return;
}

final StringJoiner message = new StringJoiner("\n");
message.add("Cannot remove plugins because the following are extended by other plugins:");
usedBy.forEach((key, value) -> {
String s = "\t" + key + " used by " + value;
message.add(s);
});

throw new UserException(PLUGIN_STILL_USED, message.toString());
return usedBy;
}

private void checkCanRemove(PluginDescriptor plugin) throws UserException {
private Tuple<RemovePluginProblem, String> canRemovePlugin(PluginDescriptor plugin) {
String pluginId = plugin.getId();
final Path pluginDir = env.pluginsFile().resolve(pluginId);
final Path pluginConfigDir = env.configFile().resolve(pluginId);
@@ -121,20 +109,40 @@ private void checkCanRemove(PluginDescriptor plugin) throws UserException {
*/
if ((Files.exists(pluginDir) == false && Files.exists(pluginConfigDir) == false && Files.exists(removing) == false)
|| (Files.exists(pluginDir) == false && Files.exists(pluginConfigDir) && this.purge == false)) {
final String message = String.format(
Locale.ROOT,
"plugin [%s] not found; run 'elasticsearch-plugin list' to get list of installed plugins",
pluginId
return Tuple.tuple(
RemovePluginProblem.NOT_FOUND,
"plugin [" + pluginId + "] not found; run 'elasticsearch-plugin list' to get list of installed plugins"
);
throw new UserException(ExitCodes.CONFIG, message);
}

final Path pluginBinDir = env.binFile().resolve(pluginId);
if (Files.exists(pluginBinDir)) {
if (Files.isDirectory(pluginBinDir) == false) {
throw new UserException(ExitCodes.IO_ERROR, "bin dir for " + pluginId + " is not a directory");
return Tuple.tuple(RemovePluginProblem.BIN_FILE_NOT_DIRECTORY, "bin dir for [" + pluginId + "] is not a directory");
}
}

return null;
}

/**
* Remove the plugin specified by {@code pluginName}. You should call {@link #checkRemovePlugins(List)}
* first, to ensure that the removal can proceed.
*
* @param plugins the IDs of the plugins to remove
* @throws IOException if any I/O exception occurs while performing a file operation
* @throws UserException if plugins is null or empty
* @throws UserException if plugin directory does not exist
* @throws UserException if the plugin bin directory is not a directory
*/
void removePlugins(List<PluginDescriptor> plugins) throws IOException, UserException {
if (plugins == null || plugins.isEmpty()) {
throw new UserException(ExitCodes.USAGE, "At least one plugin ID is required");
}

for (PluginDescriptor plugin : plugins) {
removePlugin(plugin);
}
}

private void removePlugin(PluginDescriptor plugin) throws IOException {
Original file line number Diff line number Diff line change
@@ -12,7 +12,10 @@
import joptsimple.OptionSpec;

import org.elasticsearch.cli.EnvironmentAwareCommand;
import org.elasticsearch.cli.ExitCodes;
import org.elasticsearch.cli.Terminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.env.Environment;

import java.util.Arrays;
@@ -23,6 +26,11 @@
* A command for the plugin CLI to remove plugins from Elasticsearch.
*/
class RemovePluginCommand extends EnvironmentAwareCommand {

// exit codes for remove
/** A plugin cannot be removed because it is extended by another plugin. */
static final int PLUGIN_STILL_USED = 11;

private final OptionSpec<Void> purgeOption;
private final OptionSpec<String> arguments;

@@ -37,6 +45,31 @@ protected void execute(final Terminal terminal, final OptionSet options, final E
final List<PluginDescriptor> plugins = arguments.values(options).stream().map(PluginDescriptor::new).collect(Collectors.toList());

final RemovePluginAction action = new RemovePluginAction(new TerminalLogger(terminal), env, options.has(purgeOption));
action.execute(plugins);

final Tuple<RemovePluginAction.RemovePluginProblem, String> problem = action.checkRemovePlugins(plugins);
if (problem != null) {
int exitCode;
switch (problem.v1()) {
case NOT_FOUND:
exitCode = ExitCodes.CONFIG;
break;

case STILL_USED:
exitCode = PLUGIN_STILL_USED;
break;

case BIN_FILE_NOT_DIRECTORY:
exitCode = ExitCodes.IO_ERROR;
break;

default:
exitCode = ExitCodes.USAGE;
break;
}

throw new UserException(exitCode, problem.v2());
}

action.removePlugins(plugins);
}
}
Original file line number Diff line number Diff line change
@@ -14,9 +14,11 @@
import org.elasticsearch.cli.MockTerminal;
import org.elasticsearch.cli.UserException;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.core.Tuple;
import org.elasticsearch.env.Environment;
import org.elasticsearch.env.TestEnvironment;
import org.elasticsearch.plugins.PluginTestUtil;
import org.elasticsearch.plugins.cli.RemovePluginAction.RemovePluginProblem;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.VersionUtils;
import org.junit.Before;
@@ -36,7 +38,6 @@
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasToString;

@LuceneTestCase.SuppressFileSystems("*")
public class RemovePluginActionTests extends ESTestCase {
@@ -52,7 +53,7 @@ private MockRemovePluginCommand(final Environment env) {
}

@Override
protected Environment createEnv(Map<String, String> settings) throws UserException {
protected Environment createEnv(Map<String, String> settings) {
return env;
}
}
@@ -99,13 +100,22 @@ static MockTerminal removePlugin(String pluginId, Path home, boolean purge) thro
return removePlugin(List.of(pluginId), home, purge);
}

static Tuple<RemovePluginProblem, String> checkRemovePlugins(List<String> pluginIds, Path home) throws Exception {
Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", home).build());
MockTerminal terminal = new MockTerminal();
final List<PluginDescriptor> plugins = pluginIds == null
? null
: pluginIds.stream().map(PluginDescriptor::new).collect(Collectors.toList());
return new RemovePluginAction(new TerminalLogger(terminal), env, false).checkRemovePlugins(plugins);
}

static MockTerminal removePlugin(List<String> pluginIds, Path home, boolean purge) throws Exception {
Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", home).build());
MockTerminal terminal = new MockTerminal();
final List<PluginDescriptor> plugins = pluginIds == null
? null
: pluginIds.stream().map(PluginDescriptor::new).collect(Collectors.toList());
new RemovePluginAction(new TerminalLogger(terminal), env, purge).execute(plugins);
new RemovePluginAction(new TerminalLogger(terminal), env, purge).removePlugins(plugins);
return terminal;
}

@@ -120,9 +130,10 @@ static void assertRemoveCleaned(Environment env) throws IOException {
}

public void testMissing() throws Exception {
UserException e = expectThrows(UserException.class, () -> removePlugin("dne", home, randomBoolean()));
assertTrue(e.getMessage(), e.getMessage().contains("plugin [dne] not found"));
assertRemoveCleaned(env);
Tuple<RemovePluginProblem, String> problem = checkRemovePlugins(List.of("dne"), home);

assertThat(problem.v1(), equalTo(RemovePluginProblem.NOT_FOUND));
assertThat(problem.v2(), equalTo("plugin [dne] not found; run 'elasticsearch-plugin list' to get list of installed plugins"));
}

public void testBasic() throws Exception {
@@ -181,11 +192,11 @@ public void testBin() throws Exception {
public void testBinNotDir() throws Exception {
createPlugin("fake");
Files.createFile(env.binFile().resolve("fake"));
UserException e = expectThrows(UserException.class, () -> removePlugin("fake", home, randomBoolean()));
assertTrue(e.getMessage(), e.getMessage().contains("not a directory"));
assertTrue(Files.exists(env.pluginsFile().resolve("fake"))); // did not remove
assertTrue(Files.exists(env.binFile().resolve("fake")));
assertRemoveCleaned(env);

Tuple<RemovePluginProblem, String> problem = checkRemovePlugins(List.of("fake"), home);

assertThat(problem.v1(), equalTo(RemovePluginProblem.BIN_FILE_NOT_DIRECTORY));
assertThat(problem.v2(), equalTo("bin dir for [fake] is not a directory"));
}

public void testConfigDirPreserved() throws Exception {
@@ -222,11 +233,6 @@ public void testPurgePluginDoesNotExist() throws Exception {
assertRemoveCleaned(env);
}

public void testPurgeNothingExists() throws Exception {
final UserException e = expectThrows(UserException.class, () -> removePlugin("fake", home, true));
assertThat(e, hasToString(containsString("plugin [fake] not found")));
}

public void testPurgeOnlyMarkerFileExists() throws Exception {
final Path configDir = env.configFile().resolve("fake");
final Path removing = env.pluginsFile().resolve(".removing-fake");
@@ -244,17 +250,16 @@ public void testNoConfigDirPreserved() throws Exception {
}

public void testRemoveUninstalledPluginErrors() throws Exception {
UserException e = expectThrows(UserException.class, () -> removePlugin("fake", home, randomBoolean()));
assertEquals(ExitCodes.CONFIG, e.exitCode);
assertEquals("plugin [fake] not found; run 'elasticsearch-plugin list' to get list of installed plugins", e.getMessage());

MockTerminal terminal = new MockTerminal();

new MockRemovePluginCommand(env) {
final int exitCode = new MockRemovePluginCommand(env) {
protected boolean addShutdownHook() {
return false;
}
}.main(new String[] { "-Epath.home=" + home, "fake" }, terminal);

assertThat(exitCode, equalTo(ExitCodes.CONFIG));

try (
BufferedReader reader = new BufferedReader(new StringReader(terminal.getOutput()));
BufferedReader errorReader = new BufferedReader(new StringReader(terminal.getErrorOutput()))