/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.plugins; import joptsimple.OptionSet; import joptsimple.OptionSpec; import org.opensearch.cli.EnvironmentAwareCommand; import org.opensearch.cli.ExitCodes; import org.opensearch.cli.Terminal; import org.opensearch.cli.UserException; import org.opensearch.common.util.io.IOUtils; import org.opensearch.env.Environment; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import static org.opensearch.cli.Terminal.Verbosity.VERBOSE; /** * A command for the plugin CLI to remove a plugin from OpenSearch. */ 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 purgeOption; private final OptionSpec arguments; RemovePluginCommand() { super("removes a plugin from OpenSearch"); this.purgeOption = parser.acceptsAll(Arrays.asList("p", "purge"), "Purge plugin configuration files"); this.arguments = parser.nonOptions("plugin name"); } @Override protected void execute(final Terminal terminal, final OptionSet options, final Environment env) throws Exception { final String pluginName = arguments.value(options); final boolean purge = options.has(purgeOption); execute(terminal, env, pluginName, purge); } /** * Remove the plugin specified by {@code pluginName}. * * @param terminal the terminal to use for input/output * @param env the environment for the local node * @param pluginName the name of the plugin to remove * @param purge if true, plugin configuration files will be removed but otherwise preserved * @throws IOException if any I/O exception occurs while performing a file operation * @throws UserException if plugin name is null * @throws UserException if plugin directory does not exist * @throws UserException if the plugin bin directory is not a directory */ void execute(Terminal terminal, Environment env, String pluginName, boolean purge) throws IOException, UserException { if (pluginName == null) { throw new UserException(ExitCodes.USAGE, "plugin name is required"); } // first make sure nothing extends this plugin List usedBy = new ArrayList<>(); Set bundles = PluginsService.getPluginBundles(env.pluginsDir()); for (PluginsService.Bundle bundle : bundles) { for (String extendedPlugin : bundle.plugin.getExtendedPlugins()) { if (extendedPlugin.equals(pluginName)) { usedBy.add(bundle.plugin.getName()); } } } if (usedBy.isEmpty() == false) { throw new UserException( PLUGIN_STILL_USED, "plugin [" + pluginName + "] cannot be removed" + " because it is extended by other plugins: " + usedBy ); } Path pluginDir = env.pluginsDir().resolve(pluginName); Path pluginConfigDir = env.configDir().resolve(pluginName); Path removing = env.pluginsDir().resolve(".removing-" + pluginName); /* * If the plugin directory is not found with the plugin name, scan the list of all installed plugins * to find if the concerned plugin is installed with a custom folder name. */ if (!Files.exists(pluginDir)) { terminal.println("searching in other folders to find if plugin exists with custom folder name"); pluginDir = PluginHelper.verifyIfPluginExists(env.pluginsDir(), pluginName); pluginConfigDir = env.configDir().resolve(pluginDir.getFileName()); removing = env.pluginsDir().resolve(".removing-" + pluginDir.getFileName()); } terminal.println("-> removing [" + pluginName + "]..."); /* * 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 * 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 * not exist, the plugin config does, and we are not purging, again fail to the user that the plugin is not found. */ if ((!Files.exists(pluginDir) && !Files.exists(pluginConfigDir) && !Files.exists(removing)) || (!Files.exists(pluginDir) && Files.exists(pluginConfigDir) && !purge)) { final String message = String.format( Locale.ROOT, "plugin [%s] not found; run 'opensearch-plugin list' to get list of installed plugins", pluginName ); throw new UserException(ExitCodes.CONFIG, message); } final List pluginPaths = new ArrayList<>(); /* * Add the contents of the plugin directory before creating the marker file and adding it to the list of paths to be deleted so * that the marker file is the last file to be deleted. */ if (Files.exists(pluginDir)) { try (Stream paths = Files.list(pluginDir)) { pluginPaths.addAll(paths.collect(Collectors.toList())); } terminal.println(VERBOSE, "removing [" + pluginDir + "]"); } final Path pluginBinDir = env.binDir().resolve(pluginName); if (Files.exists(pluginBinDir)) { if (!Files.isDirectory(pluginBinDir)) { throw new UserException(ExitCodes.IO_ERROR, "bin dir for " + pluginName + " is not a directory"); } try (Stream paths = Files.list(pluginBinDir)) { pluginPaths.addAll(paths.collect(Collectors.toList())); } pluginPaths.add(pluginBinDir); terminal.println(VERBOSE, "removing [" + pluginBinDir + "]"); } if (Files.exists(pluginConfigDir)) { if (purge) { try (Stream paths = Files.list(pluginConfigDir)) { pluginPaths.addAll(paths.collect(Collectors.toList())); } pluginPaths.add(pluginConfigDir); terminal.println(VERBOSE, "removing [" + pluginConfigDir + "]"); } else { /* * By default we preserve the config files in case the user is upgrading the plugin, but we print a message so the user * knows in case they want to remove manually. */ final String message = String.format( Locale.ROOT, "-> preserving plugin config files [%s] in case of upgrade; use --purge if not needed", pluginConfigDir ); terminal.println(message); } } /* * We are going to create a marker file in the plugin directory that indicates that this plugin is a state of removal. If the * removal fails, the existence of this marker file indicates that the plugin is in a garbage state. We check for existence of this * 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 * anything destructive, so we create the marker file as the last action before executing destructive operations. We place this * marker file in the root plugin directory (not the specific plugin directory) so that we do not have to create the specific plugin * directory if it does not exist (we are purging configuration files). */ try { Files.createFile(removing); } catch (final FileAlreadyExistsException e) { /* * We need to suppress the marker file already existing as we could be in this state if a previous removal attempt failed and * the user is attempting to remove the plugin again. */ terminal.println(VERBOSE, "marker file [" + removing + "] already exists"); } // add the plugin directory pluginPaths.add(pluginDir); // finally, add the marker file pluginPaths.add(removing); IOUtils.rm(pluginPaths.toArray(new Path[0])); } }