Skip to content

Commit da4b249

Browse files
committed
Add purge option to remove plugin CLI
By default, the remove plugin CLI command preserves configuration files. This is so that if a user is upgrading the plugin (which is done by first removing the old version and then installing the new version) they do not lose their configuration file. Yet, there are circumstances where preserving the configuration file is not desired. This commit adds a purge option to the remove plugin CLI command. Relates #24981
1 parent 3caabee commit da4b249

File tree

7 files changed

+198
-88
lines changed

7 files changed

+198
-88
lines changed

core/src/main/java/org/elasticsearch/plugins/PluginsService.java

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import java.util.Collections;
5353
import java.util.HashMap;
5454
import java.util.HashSet;
55+
import java.util.Iterator;
5556
import java.util.LinkedHashSet;
5657
import java.util.List;
5758
import java.util.Locale;
@@ -60,6 +61,7 @@
6061
import java.util.Set;
6162
import java.util.function.Function;
6263
import java.util.stream.Collectors;
64+
import java.util.stream.Stream;
6365

6466
import static org.elasticsearch.common.io.FileSystemUtils.isAccessibleDirectory;
6567

@@ -288,6 +290,27 @@ static Set<Bundle> getModuleBundles(Path modulesDirectory) throws IOException {
288290
return bundles;
289291
}
290292

293+
static void checkForFailedPluginRemovals(final Path pluginsDirectory) throws IOException {
294+
/*
295+
* Check for the existence of a marker file that indicates any plugins are in a garbage state from a failed attempt to remove the
296+
* plugin.
297+
*/
298+
try (DirectoryStream<Path> stream = Files.newDirectoryStream(pluginsDirectory, ".removing-*")) {
299+
final Iterator<Path> iterator = stream.iterator();
300+
if (iterator.hasNext()) {
301+
final Path removing = iterator.next();
302+
final String fileName = removing.getFileName().toString();
303+
final String name = fileName.substring(1 + fileName.indexOf("-"));
304+
final String message = String.format(
305+
Locale.ROOT,
306+
"found file [%s] from a failed attempt to remove the plugin [%s]; execute [elasticsearch-plugin remove %2$s]",
307+
removing,
308+
name);
309+
throw new IllegalStateException(message);
310+
}
311+
}
312+
}
313+
291314
static Set<Bundle> getPluginBundles(Path pluginsDirectory) throws IOException {
292315
Logger logger = Loggers.getLogger(PluginsService.class);
293316

@@ -298,6 +321,8 @@ static Set<Bundle> getPluginBundles(Path pluginsDirectory) throws IOException {
298321

299322
Set<Bundle> bundles = new LinkedHashSet<>();
300323

324+
checkForFailedPluginRemovals(pluginsDirectory);
325+
301326
try (DirectoryStream<Path> stream = Files.newDirectoryStream(pluginsDirectory)) {
302327
for (Path plugin : stream) {
303328
logger.trace("--- adding plugin [{}]", plugin.toAbsolutePath());
@@ -308,19 +333,6 @@ static Set<Bundle> getPluginBundles(Path pluginsDirectory) throws IOException {
308333
throw new IllegalStateException("Could not load plugin descriptor for existing plugin ["
309334
+ plugin.getFileName() + "]. Was the plugin built before 2.0?", e);
310335
}
311-
/*
312-
* Check for the existence of a marker file that indicates the plugin is in a garbage state from a failed attempt to remove
313-
* the plugin.
314-
*/
315-
final Path removing = plugin.resolve(".removing-" + info.getName());
316-
if (Files.exists(removing)) {
317-
final String message = String.format(
318-
Locale.ROOT,
319-
"found file [%s] from a failed attempt to remove the plugin [%s]; execute [elasticsearch-plugin remove %2$s]",
320-
removing,
321-
info.getName());
322-
throw new IllegalStateException(message);
323-
}
324336

325337
Set<URL> urls = new LinkedHashSet<>();
326338
try (DirectoryStream<Path> jarStream = Files.newDirectoryStream(plugin, "*.jar")) {

core/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,8 @@ public void testStartupWithRemovingMarker() throws IOException {
132132
final Path fake = home.resolve("plugins").resolve("fake");
133133
Files.createDirectories(fake);
134134
Files.createFile(fake.resolve("plugin.jar"));
135-
final Path removing = fake.resolve(".removing-fake");
136-
Files.createFile(fake.resolve(".removing-fake"));
135+
final Path removing = home.resolve("plugins").resolve(".removing-fake");
136+
Files.createFile(removing);
137137
PluginTestUtil.writeProperties(
138138
fake,
139139
"description", "fake",

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,8 @@ private PluginInfo verify(Terminal terminal, Path pluginRoot, boolean isBatch, E
485485
throw new UserException(PLUGIN_EXISTS, message);
486486
}
487487

488+
PluginsService.checkForFailedPluginRemovals(env.pluginsFile());
489+
488490
terminal.println(VERBOSE, info.toString());
489491

490492
// don't let user install plugin as a module...

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

Lines changed: 71 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,14 @@
2626
import org.elasticsearch.cli.ExitCodes;
2727
import org.elasticsearch.cli.Terminal;
2828
import org.elasticsearch.cli.UserException;
29-
import org.elasticsearch.common.Strings;
3029
import org.elasticsearch.env.Environment;
3130

3231
import java.io.IOException;
3332
import java.nio.file.FileAlreadyExistsException;
3433
import java.nio.file.Files;
3534
import java.nio.file.Path;
3635
import java.util.ArrayList;
36+
import java.util.Arrays;
3737
import java.util.List;
3838
import java.util.Locale;
3939
import java.util.stream.Collectors;
@@ -46,75 +46,113 @@
4646
*/
4747
class RemovePluginCommand extends EnvironmentAwareCommand {
4848

49+
private final OptionSpec<Void> purgeOption;
4950
private final OptionSpec<String> arguments;
5051

5152
RemovePluginCommand() {
5253
super("removes a plugin from Elasticsearch");
54+
this.purgeOption = parser.acceptsAll(Arrays.asList("p", "purge"), "Purge plugin configuration files");
5355
this.arguments = parser.nonOptions("plugin name");
5456
}
5557

5658
@Override
57-
protected void execute(final Terminal terminal, final OptionSet options, final Environment env)
58-
throws Exception {
59+
protected void execute(final Terminal terminal, final OptionSet options, final Environment env) throws Exception {
5960
final String pluginName = arguments.value(options);
60-
execute(terminal, pluginName, env);
61+
final boolean purge = options.has(purgeOption);
62+
execute(terminal, env, pluginName, purge);
6163
}
6264

6365
/**
6466
* Remove the plugin specified by {@code pluginName}.
6567
*
6668
* @param terminal the terminal to use for input/output
67-
* @param pluginName the name of the plugin to remove
6869
* @param env the environment for the local node
70+
* @param pluginName the name of the plugin to remove
71+
* @param purge if true, plugin configuration files will be removed but otherwise preserved
6972
* @throws IOException if any I/O exception occurs while performing a file operation
7073
* @throws UserException if plugin name is null
7174
* @throws UserException if plugin directory does not exist
7275
* @throws UserException if the plugin bin directory is not a directory
7376
*/
74-
void execute(final Terminal terminal, final String pluginName, final Environment env)
75-
throws IOException, UserException {
77+
void execute(
78+
final Terminal terminal,
79+
final Environment env,
80+
final String pluginName,
81+
final boolean purge) throws IOException, UserException {
7682
if (pluginName == null) {
7783
throw new UserException(ExitCodes.USAGE, "plugin name is required");
7884
}
7985

80-
terminal.println("-> removing [" + Strings.coalesceToEmpty(pluginName) + "]...");
86+
terminal.println("-> removing [" + pluginName + "]...");
8187

8288
final Path pluginDir = env.pluginsFile().resolve(pluginName);
83-
if (Files.exists(pluginDir) == false) {
89+
final Path pluginConfigDir = env.configFile().resolve(pluginName);
90+
final Path removing = env.pluginsFile().resolve(".removing-" + pluginName);
91+
/*
92+
* If the plugin does not exist and the plugin config does not exist, fail to the user that the plugin is not found, unless there's
93+
* a marker file left from a previously failed attempt in which case we proceed to clean up the marker file. Or, if the plugin does
94+
* not exist, the plugin config does, and we are not purging, again fail to the user that the plugin is not found.
95+
*/
96+
if ((!Files.exists(pluginDir) && !Files.exists(pluginConfigDir) && !Files.exists(removing))
97+
|| (!Files.exists(pluginDir) && Files.exists(pluginConfigDir) && !purge)) {
8498
final String message = String.format(
85-
Locale.ROOT,
86-
"plugin [%s] not found; "
87-
+ "run 'elasticsearch-plugin list' to get list of installed plugins",
88-
pluginName);
99+
Locale.ROOT, "plugin [%s] not found; run 'elasticsearch-plugin list' to get list of installed plugins", pluginName);
89100
throw new UserException(ExitCodes.CONFIG, message);
90101
}
91102

92103
final List<Path> pluginPaths = new ArrayList<>();
93104

105+
/*
106+
* Add the contents of the plugin directory before creating the marker file and adding it to the list of paths to be deleted so
107+
* that the marker file is the last file to be deleted.
108+
*/
109+
if (Files.exists(pluginDir)) {
110+
try (Stream<Path> paths = Files.list(pluginDir)) {
111+
pluginPaths.addAll(paths.collect(Collectors.toList()));
112+
}
113+
terminal.println(VERBOSE, "removing [" + pluginDir + "]");
114+
}
115+
94116
final Path pluginBinDir = env.binFile().resolve(pluginName);
95117
if (Files.exists(pluginBinDir)) {
96-
if (Files.isDirectory(pluginBinDir) == false) {
97-
throw new UserException(
98-
ExitCodes.IO_ERROR, "bin dir for " + pluginName + " is not a directory");
118+
if (!Files.isDirectory(pluginBinDir)) {
119+
throw new UserException(ExitCodes.IO_ERROR, "bin dir for " + pluginName + " is not a directory");
120+
}
121+
try (Stream<Path> paths = Files.list(pluginBinDir)) {
122+
pluginPaths.addAll(paths.collect(Collectors.toList()));
99123
}
100124
pluginPaths.add(pluginBinDir);
101125
terminal.println(VERBOSE, "removing [" + pluginBinDir + "]");
102126
}
103127

104-
terminal.println(VERBOSE, "removing [" + pluginDir + "]");
105-
/*
128+
if (Files.exists(pluginConfigDir)) {
129+
if (purge) {
130+
try (Stream<Path> paths = Files.list(pluginConfigDir)) {
131+
pluginPaths.addAll(paths.collect(Collectors.toList()));
132+
}
133+
pluginPaths.add(pluginConfigDir);
134+
terminal.println(VERBOSE, "removing [" + pluginConfigDir + "]");
135+
} else {
136+
/*
137+
* By default we preserve the config files in case the user is upgrading the plugin, but we print a message so the user
138+
* knows in case they want to remove manually.
139+
*/
140+
final String message = String.format(
141+
Locale.ROOT,
142+
"-> preserving plugin config files [%s] in case of upgrade; use --purge if not needed",
143+
pluginConfigDir);
144+
terminal.println(message);
145+
}
146+
}
147+
148+
/*
106149
* We are going to create a marker file in the plugin directory that indicates that this plugin is a state of removal. If the
107150
* removal fails, the existence of this marker file indicates that the plugin is in a garbage state. We check for existence of this
108-
* marker file during startup so that we do not startup with plugins in such a garbage state.
151+
* marker file during startup so that we do not startup with plugins in such a garbage state. Up to this point, we have not done
152+
* anything destructive, so we create the marker file as the last action before executing destructive operations. We place this
153+
* marker file in the root plugin directory (not the specific plugin directory) so that we do not have to create the specific plugin
154+
* directory if it does not exist (we are purging configuration files).
109155
*/
110-
final Path removing = pluginDir.resolve(".removing-" + pluginName);
111-
/*
112-
* Add the contents of the plugin directory before creating the marker file and adding it to the list of paths to be deleted so
113-
* that the marker file is the last file to be deleted.
114-
*/
115-
try (Stream<Path> paths = Files.list(pluginDir)) {
116-
pluginPaths.addAll(paths.collect(Collectors.toList()));
117-
}
118156
try {
119157
Files.createFile(removing);
120158
} catch (final FileAlreadyExistsException e) {
@@ -124,24 +162,14 @@ void execute(final Terminal terminal, final String pluginName, final Environment
124162
*/
125163
terminal.println(VERBOSE, "marker file [" + removing + "] already exists");
126164
}
127-
// now add the marker file
128-
pluginPaths.add(removing);
129-
// finally, add the plugin directory
165+
166+
// add the plugin directory
130167
pluginPaths.add(pluginDir);
131-
IOUtils.rm(pluginPaths.toArray(new Path[pluginPaths.size()]));
132168

133-
/*
134-
* We preserve the config files in case the user is upgrading the plugin, but we print a
135-
* message so the user knows in case they want to remove manually.
136-
*/
137-
final Path pluginConfigDir = env.configFile().resolve(pluginName);
138-
if (Files.exists(pluginConfigDir)) {
139-
final String message = String.format(
140-
Locale.ROOT,
141-
"-> preserving plugin config files [%s] in case of upgrade; delete manually if not needed",
142-
pluginConfigDir);
143-
terminal.println(message);
144-
}
169+
// finally, add the marker file
170+
pluginPaths.add(removing);
171+
172+
IOUtils.rm(pluginPaths.toArray(new Path[pluginPaths.size()]));
145173
}
146174

147175
}

0 commit comments

Comments
 (0)