Merge pull request #16336 from rjernst/demangle

Reduce complexity of plugin cli
This commit is contained in:
Ryan Ernst 2016-02-01 07:20:04 -08:00
commit 661e3c99f7
27 changed files with 1327 additions and 3063 deletions

View File

@ -50,7 +50,7 @@ public class HelpPrinter {
}
});
} catch (IOException ioe) {
ioe.printStackTrace(terminal.writer());
throw new RuntimeException(ioe);
}
terminal.println();
}

View File

@ -132,8 +132,6 @@ public abstract class Terminal {
protected abstract void doPrint(String msg, Object... args);
public abstract PrintWriter writer();
private static class ConsoleTerminal extends Terminal {
final Console console = System.console();
@ -158,11 +156,6 @@ public abstract class Terminal {
return console.readPassword(text, args);
}
@Override
public PrintWriter writer() {
return console.writer();
}
@Override
public void printStackTrace(Throwable t) {
t.printStackTrace(console.writer());
@ -199,10 +192,5 @@ public abstract class Terminal {
public void printStackTrace(Throwable t) {
t.printStackTrace(printWriter);
}
@Override
public PrintWriter writer() {
return printWriter;
}
}
}

View File

@ -1,488 +0,0 @@
/*
* 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.
*/
package org.elasticsearch.common.http.client;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.Build;
import org.elasticsearch.ElasticsearchCorruptionException;
import org.elasticsearch.ElasticsearchTimeoutException;
import org.elasticsearch.Version;
import org.elasticsearch.common.Base64;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.hash.MessageDigests;
import org.elasticsearch.common.unit.TimeValue;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.util.List;
/**
*
*/
public class HttpDownloadHelper {
private boolean useTimestamp = false;
private boolean skipExisting = false;
public boolean download(URL source, Path dest, @Nullable DownloadProgress progress, TimeValue timeout) throws Exception {
if (Files.exists(dest) && skipExisting) {
return true;
}
//don't do any progress, unless asked
if (progress == null) {
progress = new NullProgress();
}
//set the timestamp to the file date.
long timestamp = 0;
boolean hasTimestamp = false;
if (useTimestamp && Files.exists(dest) ) {
timestamp = Files.getLastModifiedTime(dest).toMillis();
hasTimestamp = true;
}
GetThread getThread = new GetThread(source, dest, hasTimestamp, timestamp, progress);
try {
getThread.setDaemon(true);
getThread.start();
getThread.join(timeout.millis());
if (getThread.isAlive()) {
throw new ElasticsearchTimeoutException("The GET operation took longer than " + timeout + ", stopping it.");
}
}
catch (InterruptedException ie) {
return false;
} finally {
getThread.closeStreams();
}
return getThread.wasSuccessful();
}
public interface Checksummer {
/** Return the hex string for the given byte array */
String checksum(byte[] filebytes);
/** Human-readable name for the checksum format */
String name();
}
/** Checksummer for SHA1 */
public static Checksummer SHA1_CHECKSUM = new Checksummer() {
@Override
public String checksum(byte[] filebytes) {
return MessageDigests.toHexString(MessageDigests.sha1().digest(filebytes));
}
@Override
public String name() {
return "SHA1";
}
};
/** Checksummer for MD5 */
public static Checksummer MD5_CHECKSUM = new Checksummer() {
@Override
public String checksum(byte[] filebytes) {
return MessageDigests.toHexString(MessageDigests.md5().digest(filebytes));
}
@Override
public String name() {
return "MD5";
}
};
/**
* Download the given checksum URL to the destination and check the checksum
* @param checksumURL URL for the checksum file
* @param originalFile original file to calculate checksum of
* @param checksumFile destination to download the checksum file to
* @param hashFunc class used to calculate the checksum of the file
* @return true if the checksum was validated, false if it did not exist
* @throws Exception if the checksum failed to match
*/
public boolean downloadAndVerifyChecksum(URL checksumURL, Path originalFile, Path checksumFile,
@Nullable DownloadProgress progress,
TimeValue timeout, Checksummer hashFunc) throws Exception {
try {
if (download(checksumURL, checksumFile, progress, timeout)) {
byte[] fileBytes = Files.readAllBytes(originalFile);
List<String> checksumLines = Files.readAllLines(checksumFile, StandardCharsets.UTF_8);
if (checksumLines.size() != 1) {
throw new ElasticsearchCorruptionException("invalid format for checksum file (" +
hashFunc.name() + "), expected 1 line, got: " + checksumLines.size());
}
String checksumHex = checksumLines.get(0);
String fileHex = hashFunc.checksum(fileBytes);
if (fileHex.equals(checksumHex) == false) {
throw new ElasticsearchCorruptionException("incorrect hash (" + hashFunc.name() +
"), file hash: [" + fileHex + "], expected: [" + checksumHex + "]");
}
return true;
}
} catch (FileNotFoundException | NoSuchFileException e) {
// checksum file doesn't exist
return false;
} finally {
IOUtils.deleteFilesIgnoringExceptions(checksumFile);
}
return false;
}
/**
* Interface implemented for reporting
* progress of downloading.
*/
public interface DownloadProgress {
/**
* begin a download
*/
void beginDownload();
/**
* tick handler
*/
void onTick();
/**
* end a download
*/
void endDownload();
}
/**
* do nothing with progress info
*/
public static class NullProgress implements DownloadProgress {
/**
* begin a download
*/
@Override
public void beginDownload() {
}
/**
* tick handler
*/
@Override
public void onTick() {
}
/**
* end a download
*/
@Override
public void endDownload() {
}
}
/**
* verbose progress system prints to some output stream
*/
public static class VerboseProgress implements DownloadProgress {
private int dots = 0;
// CheckStyle:VisibilityModifier OFF - bc
PrintWriter writer;
// CheckStyle:VisibilityModifier ON
/**
* Construct a verbose progress reporter.
*
* @param writer the output stream.
*/
public VerboseProgress(PrintWriter writer) {
this.writer = writer;
}
/**
* begin a download
*/
@Override
public void beginDownload() {
writer.print("Downloading ");
dots = 0;
}
/**
* tick handler
*/
@Override
public void onTick() {
writer.print(".");
if (dots++ > 50) {
writer.flush();
dots = 0;
}
}
/**
* end a download
*/
@Override
public void endDownload() {
writer.println("DONE");
writer.flush();
}
}
private class GetThread extends Thread {
private final URL source;
private final Path dest;
private final boolean hasTimestamp;
private final long timestamp;
private final DownloadProgress progress;
private boolean success = false;
private IOException ioexception = null;
private InputStream is = null;
private OutputStream os = null;
private URLConnection connection;
private int redirections = 0;
GetThread(URL source, Path dest, boolean h, long t, DownloadProgress p) {
this.source = source;
this.dest = dest;
hasTimestamp = h;
timestamp = t;
progress = p;
}
@Override
public void run() {
try {
success = get();
} catch (IOException ioex) {
ioexception = ioex;
}
}
private boolean get() throws IOException {
connection = openConnection(source);
if (connection == null) {
return false;
}
boolean downloadSucceeded = downloadFile();
//if (and only if) the use file time option is set, then
//the saved file now has its timestamp set to that of the
//downloaded file
if (downloadSucceeded && useTimestamp) {
updateTimeStamp();
}
return downloadSucceeded;
}
private boolean redirectionAllowed(URL aSource, URL aDest) throws IOException {
// Argh, github does this...
// if (!(aSource.getProtocol().equals(aDest.getProtocol()) || ("http"
// .equals(aSource.getProtocol()) && "https".equals(aDest
// .getProtocol())))) {
// String message = "Redirection detected from "
// + aSource.getProtocol() + " to " + aDest.getProtocol()
// + ". Protocol switch unsafe, not allowed.";
// throw new IOException(message);
// }
redirections++;
if (redirections > 5) {
String message = "More than " + 5 + " times redirected, giving up";
throw new IOException(message);
}
return true;
}
private URLConnection openConnection(URL aSource) throws IOException {
// set up the URL connection
URLConnection connection = aSource.openConnection();
// modify the headers
// NB: things like user authentication could go in here too.
if (hasTimestamp) {
connection.setIfModifiedSince(timestamp);
}
// in case the plugin manager is its own project, this can become an authenticator
boolean isSecureProcotol = "https".equalsIgnoreCase(aSource.getProtocol());
boolean isAuthInfoSet = !Strings.isNullOrEmpty(aSource.getUserInfo());
if (isAuthInfoSet) {
if (!isSecureProcotol) {
throw new IOException("Basic auth is only supported for HTTPS!");
}
String basicAuth = Base64.encodeBytes(aSource.getUserInfo().getBytes(StandardCharsets.UTF_8));
connection.setRequestProperty("Authorization", "Basic " + basicAuth);
}
if (connection instanceof HttpURLConnection) {
((HttpURLConnection) connection).setInstanceFollowRedirects(false);
connection.setUseCaches(true);
connection.setConnectTimeout(5000);
}
connection.setRequestProperty("ES-Version", Version.CURRENT.toString());
connection.setRequestProperty("ES-Build-Hash", Build.CURRENT.shortHash());
connection.setRequestProperty("User-Agent", "elasticsearch-plugin-manager");
// connect to the remote site (may take some time)
connection.connect();
// First check on a 301 / 302 (moved) response (HTTP only)
if (connection instanceof HttpURLConnection) {
HttpURLConnection httpConnection = (HttpURLConnection) connection;
int responseCode = httpConnection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_MOVED_PERM ||
responseCode == HttpURLConnection.HTTP_MOVED_TEMP ||
responseCode == HttpURLConnection.HTTP_SEE_OTHER) {
String newLocation = httpConnection.getHeaderField("Location");
URL newURL = new URL(newLocation);
if (!redirectionAllowed(aSource, newURL)) {
return null;
}
return openConnection(newURL);
}
// next test for a 304 result (HTTP only)
long lastModified = httpConnection.getLastModified();
if (responseCode == HttpURLConnection.HTTP_NOT_MODIFIED
|| (lastModified != 0 && hasTimestamp && timestamp >= lastModified)) {
// not modified so no file download. just return
// instead and trace out something so the user
// doesn't think that the download happened when it
// didn't
return null;
}
// test for 401 result (HTTP only)
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
String message = "HTTP Authorization failure";
throw new IOException(message);
}
}
//REVISIT: at this point even non HTTP connections may
//support the if-modified-since behaviour -we just check
//the date of the content and skip the write if it is not
//newer. Some protocols (FTP) don't include dates, of
//course.
return connection;
}
private boolean downloadFile() throws FileNotFoundException, IOException {
IOException lastEx = null;
for (int i = 0; i < 3; i++) {
// this three attempt trick is to get round quirks in different
// Java implementations. Some of them take a few goes to bind
// property; we ignore the first couple of such failures.
try {
is = connection.getInputStream();
break;
} catch (IOException ex) {
lastEx = ex;
}
}
if (is == null) {
throw lastEx;
}
os = Files.newOutputStream(dest);
progress.beginDownload();
boolean finished = false;
try {
byte[] buffer = new byte[1024 * 100];
int length;
while (!isInterrupted() && (length = is.read(buffer)) >= 0) {
os.write(buffer, 0, length);
progress.onTick();
}
finished = !isInterrupted();
} finally {
if (!finished) {
// we have started to (over)write dest, but failed.
// Try to delete the garbage we'd otherwise leave
// behind.
IOUtils.closeWhileHandlingException(os, is);
IOUtils.deleteFilesIgnoringExceptions(dest);
} else {
IOUtils.close(os, is);
}
}
progress.endDownload();
return true;
}
private void updateTimeStamp() throws IOException {
long remoteTimestamp = connection.getLastModified();
if (remoteTimestamp != 0) {
Files.setLastModifiedTime(dest, FileTime.fromMillis(remoteTimestamp));
}
}
/**
* Has the download completed successfully?
* <p>
* Re-throws any exception caught during executaion.</p>
*/
boolean wasSuccessful() throws IOException {
if (ioexception != null) {
throw ioexception;
}
return success;
}
/**
* Closes streams, interrupts the download, may delete the
* output file.
*/
void closeStreams() throws IOException {
interrupt();
if (success) {
IOUtils.close(is, os);
} else {
IOUtils.closeWhileHandlingException(is, os);
if (dest != null && Files.exists(dest)) {
IOUtils.deleteFilesIgnoringExceptions(dest);
}
}
}
}
}

View File

@ -52,33 +52,6 @@ public final class FileSystemUtils {
private FileSystemUtils() {} // only static methods
/**
* Returns <code>true</code> iff a file under the given root has one of the given extensions. This method
* will travers directories recursively and will terminate once any of the extensions was found. This
* methods will not follow any links.
*
* @param root the root directory to travers. Must be a directory
* @param extensions the file extensions to look for
* @return <code>true</code> iff a file under the given root has one of the given extensions, otherwise <code>false</code>
* @throws IOException if an IOException occurs or if the given root path is not a directory.
*/
public static boolean hasExtensions(Path root, final String... extensions) throws IOException {
final AtomicBoolean retVal = new AtomicBoolean(false);
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
for (String extension : extensions) {
if (file.getFileName().toString().endsWith(extension)) {
retVal.set(true);
return FileVisitResult.TERMINATE;
}
}
return super.visitFile(file, attrs);
}
});
return retVal.get();
}
/**
* Returns <code>true</code> iff one of the files exists otherwise <code>false</code>
*/
@ -168,167 +141,6 @@ public final class FileSystemUtils {
return new BufferedReader(reader);
}
/**
* This utility copy a full directory content (excluded) under
* a new directory but without overwriting existing files.
*
* When a file already exists in destination dir, the source file is copied under
* destination directory but with a suffix appended if set or source file is ignored
* if suffix is not set (null).
* @param source Source directory (for example /tmp/es/src)
* @param destination Destination directory (destination directory /tmp/es/dst)
* @param suffix When not null, files are copied with a suffix appended to the original name (eg: ".new")
* When null, files are ignored
*/
public static void moveFilesWithoutOverwriting(Path source, final Path destination, final String suffix) throws IOException {
// Create destination dir
Files.createDirectories(destination);
final int configPathRootLevel = source.getNameCount();
// We walk through the file tree from
Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
private Path buildPath(Path path) {
return destination.resolve(path);
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
// We are now in dir. We need to remove root of config files to have a relative path
// If we are not walking in root dir, we might be able to copy its content
// if it does not already exist
if (configPathRootLevel != dir.getNameCount()) {
Path subpath = dir.subpath(configPathRootLevel, dir.getNameCount());
Path path = buildPath(subpath);
if (!Files.exists(path)) {
// We just move the structure to new dir
// we can't do atomic move here since src / dest might be on different mounts?
move(dir, path);
// We just ignore sub files from here
return FileVisitResult.SKIP_SUBTREE;
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path subpath = null;
if (configPathRootLevel != file.getNameCount()) {
subpath = file.subpath(configPathRootLevel, file.getNameCount());
}
Path path = buildPath(subpath);
if (!Files.exists(path)) {
// We just move the new file to new dir
move(file, path);
} else if (suffix != null) {
if (!isSameFile(file, path)) {
// If it already exists we try to copy this new version appending suffix to its name
path = path.resolveSibling(path.getFileName().toString().concat(suffix));
// We just move the file to new dir but with a new name (appended with suffix)
Files.move(file, path, StandardCopyOption.REPLACE_EXISTING);
}
}
return FileVisitResult.CONTINUE;
}
/**
* Compares the content of two paths by comparing them
*/
private boolean isSameFile(Path first, Path second) throws IOException {
// do quick file size comparison before hashing
boolean sameFileSize = Files.size(first) == Files.size(second);
if (!sameFileSize) {
return false;
}
byte[] firstBytes = Files.readAllBytes(first);
byte[] secondBytes = Files.readAllBytes(second);
return Arrays.equals(firstBytes, secondBytes);
}
});
}
/**
* Copy recursively a dir to a new location
* @param source source dir
* @param destination destination dir
*/
public static void copyDirectoryRecursively(Path source, Path destination) throws IOException {
Files.walkFileTree(source, new TreeCopier(source, destination, false));
}
/**
* Move or rename a file to a target file. This method supports moving a file from
* different filesystems (not supported by Files.move()).
*
* @param source source file
* @param destination destination file
*/
public static void move(Path source, Path destination) throws IOException {
try {
// We can't use atomic move here since source & target can be on different filesystems.
Files.move(source, destination);
} catch (DirectoryNotEmptyException e) {
Files.walkFileTree(source, new TreeCopier(source, destination, true));
}
}
// TODO: note that this will fail if source and target are on different NIO.2 filesystems.
static class TreeCopier extends SimpleFileVisitor<Path> {
private final Path source;
private final Path target;
private final boolean delete;
TreeCopier(Path source, Path target, boolean delete) {
this.source = source;
this.target = target;
this.delete = delete;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
Path newDir = target.resolve(source.relativize(dir));
try {
Files.copy(dir, newDir);
} catch (FileAlreadyExistsException x) {
// We ignore this
} catch (IOException x) {
return SKIP_SUBTREE;
}
return CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (delete) {
IOUtils.rm(dir);
}
return CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Path newFile = target.resolve(source.relativize(file));
try {
Files.copy(file, newFile);
if (delete) {
Files.deleteIfExists(file);
}
} catch (IOException x) {
// We ignore this
}
return CONTINUE;
}
}
/**
* Returns an array of all files in the given directory matching.
*/

View File

@ -25,7 +25,7 @@ import org.apache.log4j.spi.LoggingEvent;
import org.elasticsearch.common.cli.Terminal;
/**
* TerminalAppender logs event to Terminal.DEFAULT. It is used for example by the PluginManagerCliParser.
* TerminalAppender logs event to Terminal.DEFAULT. It is used for example by the PluginCli.
* */
public class TerminalAppender extends AppenderSkeleton {
@Override

View File

@ -0,0 +1,402 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.Build;
import org.elasticsearch.Version;
import org.elasticsearch.bootstrap.JarHell;
import org.elasticsearch.common.cli.CliTool;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.hash.MessageDigests;
import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import static java.util.Collections.unmodifiableSet;
import static org.elasticsearch.common.cli.Terminal.Verbosity.VERBOSE;
import static org.elasticsearch.common.util.set.Sets.newHashSet;
/**
* A command for the plugin cli to install a plugin into elasticsearch.
*
* The install command takes a plugin id, which may be any of the following:
* <ul>
* <li>An official elasticsearch plugin name</li>
* <li>Maven coordinates to a plugin zip</li>
* <li>A URL to a plugin zip</li>
* </ul>
*
* Plugins are packaged as zip files. Each packaged plugin must contain a
* plugin properties file. See {@link PluginInfo}.
* <p>
* The installation process first extracts the plugin files into a temporary
* directory in order to verify the plugin satisfies the following requirements:
* <ul>
* <li>Jar hell does not exist, either between the plugin's own jars, or with elasticsearch</li>
* <li>The plugin is not a module already provided with elasticsearch</li>
* <li>If the plugin contains extra security permissions, the policy file is validated</li>
* </ul>
* <p>
* A plugin may also contain an optional {@code bin} directory which contains scripts. The
* scripts will be installed into a subdirectory of the elasticsearch bin directory, using
* the name of the plugin, and the scripts will be marked executable.
* <p>
* A plugin may also contain an optional {@code config} directory which contains configuration
* files specific to the plugin. The config files be installed into a subdirectory of the
* elasticsearch config directory, using the name of the plugin. If any files to be installed
* already exist, they will be skipped.
*/
class InstallPluginCommand extends CliTool.Command {
private static final String PROPERTY_SUPPORT_STAGING_URLS = "es.plugins.staging";
// TODO: make this a resource file generated by gradle
static final Set<String> MODULES = unmodifiableSet(newHashSet(
"lang-expression",
"lang-groovy"));
// TODO: make this a resource file generated by gradle
static final Set<String> OFFICIAL_PLUGINS = unmodifiableSet(newHashSet(
"analysis-icu",
"analysis-kuromoji",
"analysis-phonetic",
"analysis-smartcn",
"analysis-stempel",
"delete-by-query",
"discovery-azure",
"discovery-ec2",
"discovery-gce",
"discovery-multicast",
"lang-javascript",
"lang-painless",
"lang-python",
"mapper-attachments",
"mapper-murmur3",
"mapper-size",
"repository-azure",
"repository-hdfs",
"repository-s3",
"store-smb"));
private final String pluginId;
private final boolean batch;
InstallPluginCommand(Terminal terminal, String pluginId, boolean batch) {
super(terminal);
this.pluginId = pluginId;
this.batch = batch;
}
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
// TODO: remove this leniency!! is it needed anymore?
if (Files.exists(env.pluginsFile()) == false) {
terminal.println("Plugins directory [%s] does not exist. Creating...", env.pluginsFile());
Files.createDirectory(env.pluginsFile());
}
if (Environment.isWritable(env.pluginsFile()) == false) {
throw new IOException("Plugins directory is read only: " + env.pluginsFile());
}
Path pluginZip = download(pluginId, env.tmpFile());
Path extractedZip = unzip(pluginZip, env.pluginsFile());
install(extractedZip, env);
return CliTool.ExitStatus.OK;
}
/** Downloads the plugin and returns the file it was downloaded to. */
private Path download(String pluginId, Path tmpDir) throws IOException {
if (OFFICIAL_PLUGINS.contains(pluginId)) {
final String version = Version.CURRENT.toString();
final String url;
if (System.getProperty(PROPERTY_SUPPORT_STAGING_URLS, "false").equals("true")) {
url = String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/staging/%1$s-%2$s/org/elasticsearch/plugin/%3$s/%1$s/%3$s-%1$s.zip",
version, Build.CURRENT.shortHash(), pluginId);
} else {
url = String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/release/org/elasticsearch/plugin/%1$s/%2$s/%1$s-%2$s.zip",
pluginId, version);
}
terminal.println("-> Downloading " + pluginId + " from elastic");
return downloadZipAndChecksum(url, tmpDir);
}
// now try as maven coordinates, a valid URL would only have a single colon
String[] coordinates = pluginId.split(":");
if (coordinates.length == 3) {
String mavenUrl = String.format(Locale.ROOT, "https://repo1.maven.org/maven2/%1$s/%2$s/%3$s/%2$s-%3$s.zip",
coordinates[0].replace(".", "/") /* groupId */, coordinates[1] /* artifactId */, coordinates[2] /* version */);
terminal.println("-> Downloading " + pluginId + " from maven central");
return downloadZipAndChecksum(mavenUrl, tmpDir);
}
// fall back to plain old URL
terminal.println("-> Downloading " + URLDecoder.decode(pluginId, "UTF-8"));
return downloadZip(pluginId, tmpDir);
}
/** Downloads a zip from the url, into a temp file under the given temp dir. */
private Path downloadZip(String urlString, Path tmpDir) throws IOException {
URL url = new URL(urlString);
Path zip = Files.createTempFile(tmpDir, null, ".zip");
try (InputStream in = url.openStream()) {
// must overwrite since creating the temp file above actually created the file
Files.copy(in, zip, StandardCopyOption.REPLACE_EXISTING);
}
return zip;
}
/** Downloads a zip from the url, as well as a SHA1 checksum, and checks the checksum. */
private Path downloadZipAndChecksum(String urlString, Path tmpDir) throws IOException {
Path zip = downloadZip(urlString, tmpDir);
URL checksumUrl = new URL(urlString + ".sha1");
final String expectedChecksum;
try (InputStream in = checksumUrl.openStream()) {
BufferedReader checksumReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
expectedChecksum = checksumReader.readLine();
if (checksumReader.readLine() != null) {
throw new IllegalArgumentException("Invalid checksum file at " + urlString.toString());
}
}
byte[] zipbytes = Files.readAllBytes(zip);
String gotChecksum = MessageDigests.toHexString(MessageDigests.sha1().digest(zipbytes));
if (expectedChecksum.equals(gotChecksum) == false) {
throw new IllegalStateException("SHA1 mismatch, expected " + expectedChecksum + " but got " + gotChecksum);
}
return zip;
}
private Path unzip(Path zip, Path pluginsDir) throws IOException {
// unzip plugin to a staging temp dir
Path target = Files.createTempDirectory(pluginsDir, ".installing-");
Files.createDirectories(target);
// TODO: we should wrap this in a try/catch and try deleting the target dir on failure?
try (ZipInputStream zipInput = new ZipInputStream(Files.newInputStream(zip))) {
ZipEntry entry;
byte[] buffer = new byte[8192];
while ((entry = zipInput.getNextEntry()) != null) {
Path targetFile = target.resolve(entry.getName());
// TODO: handle name being an absolute path
// be on the safe side: do not rely on that directories are always extracted
// before their children (although this makes sense, but is it guaranteed?)
Files.createDirectories(targetFile.getParent());
if (entry.isDirectory() == false) {
try (OutputStream out = Files.newOutputStream(targetFile)) {
int len;
while((len = zipInput.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
}
}
zipInput.closeEntry();
}
}
return target;
}
/** Load information about the plugin, and verify it can be installed with no errors. */
private PluginInfo verify(Path pluginRoot, Environment env) throws Exception {
// read and validate the plugin descriptor
PluginInfo info = PluginInfo.readFromProperties(pluginRoot);
terminal.println(VERBOSE, "%s", info);
// don't let luser install plugin as a module...
// they might be unavoidably in maven central and are packaged up the same way)
if (MODULES.contains(info.getName())) {
throw new IOException("plugin '" + info.getName() + "' cannot be installed like this, it is a system module");
}
// check for jar hell before any copying
jarHellCheck(pluginRoot, env.pluginsFile(), info.isIsolated());
// read optional security policy (extra permissions)
// if it exists, confirm or warn the user
Path policy = pluginRoot.resolve(PluginInfo.ES_PLUGIN_POLICY);
if (Files.exists(policy)) {
PluginSecurity.readPolicy(policy, terminal, env, batch);
}
return info;
}
/** check a candidate plugin for jar hell before installing it */
private void jarHellCheck(Path candidate, Path pluginsDir, boolean isolated) throws Exception {
// create list of current jars in classpath
final List<URL> jars = new ArrayList<>();
jars.addAll(Arrays.asList(JarHell.parseClassPath()));
// read existing bundles. this does some checks on the installation too.
List<PluginsService.Bundle> bundles = PluginsService.getPluginBundles(pluginsDir);
// if we aren't isolated, we need to jarhellcheck against any other non-isolated plugins
// thats always the first bundle
if (isolated == false) {
jars.addAll(bundles.get(0).urls);
}
// add plugin jars to the list
Path pluginJars[] = FileSystemUtils.files(candidate, "*.jar");
for (Path jar : pluginJars) {
jars.add(jar.toUri().toURL());
}
// TODO: no jars should be an error
// TODO: verify the classname exists in one of the jars!
// check combined (current classpath + new jars to-be-added)
JarHell.checkJarHell(jars.toArray(new URL[jars.size()]));
}
/**
* Installs the plugin from {@code tmpRoot} into the plugins dir.
* If the plugin has a bin dir and/or a config dir, those are copied.
*/
private void install(Path tmpRoot, Environment env) throws Exception {
List<Path> deleteOnFailure = new ArrayList<>();
deleteOnFailure.add(tmpRoot);
try {
PluginInfo info = verify(tmpRoot, env);
final Path destination = env.pluginsFile().resolve(info.getName());
if (Files.exists(destination)) {
throw new IOException("plugin directory " + destination.toAbsolutePath() + " already exists. To update the plugin, uninstall it first using 'remove " + info.getName() + "' command");
}
Path tmpBinDir = tmpRoot.resolve("bin");
if (Files.exists(tmpBinDir)) {
Path destBinDir = env.binFile().resolve(info.getName());
deleteOnFailure.add(destBinDir);
installBin(info, tmpBinDir, destBinDir);
}
Path tmpConfigDir = tmpRoot.resolve("config");
if (Files.exists(tmpConfigDir)) {
// some files may already exist, and we don't remove plugin config files on plugin removal,
// so any installed config files are left on failure too
installConfig(info, tmpConfigDir, env.configFile().resolve(info.getName()));
}
Files.move(tmpRoot, destination, StandardCopyOption.ATOMIC_MOVE);
terminal.println("-> Installed " + info.getName());
} catch (Exception installProblem) {
try {
IOUtils.rm(deleteOnFailure.toArray(new Path[0]));
} catch (IOException exceptionWhileRemovingFiles) {
installProblem.addSuppressed(exceptionWhileRemovingFiles);
}
throw installProblem;
}
}
/** Copies the files from {@code tmpBinDir} into {@code destBinDir}, along with permissions from dest dirs parent. */
private void installBin(PluginInfo info, Path tmpBinDir, Path destBinDir) throws IOException {
if (Files.isDirectory(tmpBinDir) == false) {
throw new IOException("bin in plugin " + info.getName() + " is not a directory");
}
Files.createDirectory(destBinDir);
// setup file attributes for the installed files to those of the parent dir
Set<PosixFilePermission> perms = new HashSet<>();
PosixFileAttributeView binAttrs = Files.getFileAttributeView(destBinDir.getParent(), PosixFileAttributeView.class);
if (binAttrs != null) {
perms = new HashSet<>(binAttrs.readAttributes().permissions());
// setting execute bits, since this just means "the file is executable", and actual execution requires read
perms.add(PosixFilePermission.OWNER_EXECUTE);
perms.add(PosixFilePermission.GROUP_EXECUTE);
perms.add(PosixFilePermission.OTHERS_EXECUTE);
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(tmpBinDir)) {
for (Path srcFile : stream) {
if (Files.isDirectory(srcFile)) {
throw new IOException("Directories not allowed in bin dir for plugin " + info.getName());
}
Path destFile = destBinDir.resolve(tmpBinDir.relativize(srcFile));
Files.copy(srcFile, destFile);
if (perms.isEmpty() == false) {
PosixFileAttributeView view = Files.getFileAttributeView(destFile, PosixFileAttributeView.class);
view.setPermissions(perms);
}
}
}
IOUtils.rm(tmpBinDir); // clean up what we just copied
}
/**
* Copies the files from {@code tmpConfigDir} into {@code destConfigDir}.
* Any files existing in both the source and destination will be skipped.
*/
private void installConfig(PluginInfo info, Path tmpConfigDir, Path destConfigDir) throws IOException {
if (Files.isDirectory(tmpConfigDir) == false) {
throw new IOException("config in plugin " + info.getName() + " is not a directory");
}
// create the plugin's config dir "if necessary"
Files.createDirectories(destConfigDir);
try (DirectoryStream<Path> stream = Files.newDirectoryStream(tmpConfigDir)) {
for (Path srcFile : stream) {
if (Files.isDirectory(srcFile)) {
throw new IOException("Directories not allowed in config dir for plugin " + info.getName());
}
Path destFile = destConfigDir.resolve(tmpConfigDir.relativize(srcFile));
if (Files.exists(destFile) == false) {
Files.copy(srcFile, destFile);
}
}
}
IOUtils.rm(tmpConfigDir); // clean up what we just copied
}
}

View File

@ -0,0 +1,56 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.elasticsearch.common.cli.CliTool;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
/**
* A command for the plugin cli to list plugins installed in elasticsearch.
*/
class ListPluginsCommand extends CliTool.Command {
ListPluginsCommand(Terminal terminal) {
super(terminal);
}
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
if (Files.exists(env.pluginsFile()) == false) {
throw new IOException("Plugins directory missing: " + env.pluginsFile());
}
terminal.println(Terminal.Verbosity.VERBOSE, "Plugins directory: " + env.pluginsFile());
try (DirectoryStream<Path> stream = Files.newDirectoryStream(env.pluginsFile())) {
for (Path plugin : stream) {
terminal.println(plugin.getFileName().toString());
}
}
return CliTool.ExitStatus.OK;
}
}

View File

@ -0,0 +1,124 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import org.apache.commons.cli.CommandLine;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.cli.CliTool;
import org.elasticsearch.common.cli.CliToolConfig;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.logging.log4j.LogConfigurator;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import java.util.Locale;
import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd;
import static org.elasticsearch.common.cli.CliToolConfig.Builder.option;
/**
* A cli tool for adding, removing and listing plugins for elasticsearch.
*/
public class PluginCli extends CliTool {
// commands
private static final String LIST_CMD_NAME = "list";
private static final String INSTALL_CMD_NAME = "install";
private static final String REMOVE_CMD_NAME = "remove";
// usage config
private static final CliToolConfig.Cmd LIST_CMD = cmd(LIST_CMD_NAME, ListPluginsCommand.class).build();
private static final CliToolConfig.Cmd INSTALL_CMD = cmd(INSTALL_CMD_NAME, InstallPluginCommand.class)
.options(option("b", "batch").required(false))
.build();
private static final CliToolConfig.Cmd REMOVE_CMD = cmd(REMOVE_CMD_NAME, RemovePluginCommand.class).build();
static final CliToolConfig CONFIG = CliToolConfig.config("plugin", PluginCli.class)
.cmds(LIST_CMD, INSTALL_CMD, REMOVE_CMD)
.build();
public static void main(String[] args) {
// initialize default for es.logger.level because we will not read the logging.yml
String loggerLevel = System.getProperty("es.logger.level", "INFO");
// Set the appender for all potential log files to terminal so that other components that use the logger print out the
// same terminal.
// The reason for this is that the plugin cli cannot be configured with a file appender because when the plugin command is
// executed there is no way of knowing where the logfiles should be placed. For example, if elasticsearch
// is run as service then the logs should be at /var/log/elasticsearch but when started from the tar they should be at es.home/logs.
// Therefore we print to Terminal.
Environment env = InternalSettingsPreparer.prepareEnvironment(Settings.builder()
.put("appender.terminal.type", "terminal")
.put("rootLogger", "${es.logger.level}, terminal")
.put("es.logger.level", loggerLevel)
.build(), Terminal.DEFAULT);
// configure but do not read the logging conf file
LogConfigurator.configure(env.settings(), false);
int status = new PluginCli(Terminal.DEFAULT).execute(args).status();
exit(status);
}
@SuppressForbidden(reason = "Allowed to exit explicitly from #main()")
private static void exit(int status) {
System.exit(status);
}
PluginCli(Terminal terminal) {
super(CONFIG, terminal);
}
@Override
protected Command parse(String cmdName, CommandLine cli) throws Exception {
switch (cmdName.toLowerCase(Locale.ROOT)) {
case LIST_CMD_NAME:
return new ListPluginsCommand(terminal);
case INSTALL_CMD_NAME:
return parseInstallPluginCommand(cli);
case REMOVE_CMD_NAME:
return parseRemovePluginCommand(cli);
default:
assert false : "can't get here as cmd name is validated before this method is called";
return exitCmd(ExitStatus.USAGE);
}
}
private Command parseInstallPluginCommand(CommandLine cli) {
String[] args = cli.getArgs();
if (args.length != 1) {
return exitCmd(ExitStatus.USAGE, terminal, "Must supply a single plugin id argument");
}
boolean batch = System.console() == null;
if (cli.hasOption("b")) {
batch = true;
}
return new InstallPluginCommand(terminal, args[0], batch);
}
private Command parseRemovePluginCommand(CommandLine cli) {
String[] args = cli.getArgs();
if (args.length != 1) {
return exitCmd(ExitStatus.USAGE, terminal, "Must supply a single plugin name argument");
}
return new RemovePluginCommand(terminal, args[0]);
}
}

View File

@ -82,7 +82,6 @@ public class PluginInfo implements Streamable, ToXContent {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Property [name] is missing in [" + descriptor + "]");
}
PluginManager.checkForForbiddenName(name);
String description = props.getProperty("description");
if (description == null) {
throw new IllegalArgumentException("Property [description] is missing for plugin [" + name + "]");

View File

@ -1,686 +0,0 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.Build;
import org.elasticsearch.ElasticsearchCorruptionException;
import org.elasticsearch.ElasticsearchTimeoutException;
import org.elasticsearch.ExceptionsHelper;
import org.elasticsearch.Version;
import org.elasticsearch.bootstrap.JarHell;
import org.elasticsearch.common.Randomness;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.http.client.HttpDownloadHelper;
import org.elasticsearch.common.io.FileSystemUtils;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.env.Environment;
import org.elasticsearch.plugins.PluginsService.Bundle;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.GroupPrincipal;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.UserPrincipal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.StreamSupport;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static java.util.Collections.unmodifiableSet;
import static org.elasticsearch.common.Strings.hasLength;
import static org.elasticsearch.common.cli.Terminal.Verbosity.VERBOSE;
import static org.elasticsearch.common.io.FileSystemUtils.moveFilesWithoutOverwriting;
import static org.elasticsearch.common.util.set.Sets.newHashSet;
/**
*
*/
public class PluginManager {
public static final String PROPERTY_SUPPORT_STAGING_URLS = "es.plugins.staging";
public enum OutputMode {
DEFAULT, SILENT, VERBOSE
}
private static final Set<String> BLACKLIST = unmodifiableSet(newHashSet(
"elasticsearch",
"elasticsearch.bat",
"elasticsearch.in.sh",
"plugin",
"plugin.bat",
"service.bat"));
static final Set<String> MODULES = unmodifiableSet(newHashSet(
"lang-expression",
"lang-groovy"));
static final Set<String> OFFICIAL_PLUGINS = unmodifiableSet(newHashSet(
"analysis-icu",
"analysis-kuromoji",
"analysis-phonetic",
"analysis-smartcn",
"analysis-stempel",
"delete-by-query",
"discovery-azure",
"discovery-ec2",
"discovery-gce",
"discovery-multicast",
"ingest-geoip",
"lang-javascript",
"lang-painless",
"lang-python",
"mapper-attachments",
"mapper-murmur3",
"mapper-size",
"repository-azure",
"repository-hdfs",
"repository-s3",
"store-smb"));
private final Environment environment;
private URL url;
private OutputMode outputMode;
private TimeValue timeout;
public PluginManager(Environment environment, URL url, OutputMode outputMode, TimeValue timeout) {
this.environment = environment;
this.url = url;
this.outputMode = outputMode;
this.timeout = timeout;
}
public void downloadAndExtract(String name, Terminal terminal, boolean batch) throws IOException {
if (name == null && url == null) {
throw new IllegalArgumentException("plugin name or url must be supplied with install.");
}
if (!Files.exists(environment.pluginsFile())) {
terminal.println("Plugins directory [%s] does not exist. Creating...", environment.pluginsFile());
Files.createDirectory(environment.pluginsFile());
}
if (!Environment.isWritable(environment.pluginsFile())) {
throw new IOException("plugin directory " + environment.pluginsFile() + " is read only");
}
PluginHandle pluginHandle;
if (name != null) {
pluginHandle = PluginHandle.parse(name);
checkForForbiddenName(pluginHandle.name);
} else {
// if we have no name but url, use temporary name that will be overwritten later
pluginHandle = new PluginHandle("temp_name" + Randomness.get().nextInt(), null, null);
}
Path pluginFile = download(pluginHandle, terminal);
extract(pluginHandle, terminal, pluginFile, batch);
}
private Path download(PluginHandle pluginHandle, Terminal terminal) throws IOException {
Path pluginFile = pluginHandle.newDistroFile(environment);
HttpDownloadHelper downloadHelper = new HttpDownloadHelper();
boolean downloaded = false;
boolean verified = false;
HttpDownloadHelper.DownloadProgress progress;
if (outputMode == OutputMode.SILENT) {
progress = new HttpDownloadHelper.NullProgress();
} else {
progress = new HttpDownloadHelper.VerboseProgress(terminal.writer());
}
// first, try directly from the URL provided
if (url != null) {
URL pluginUrl = url;
boolean isSecureProcotol = "https".equalsIgnoreCase(pluginUrl.getProtocol());
boolean isAuthInfoSet = !Strings.isNullOrEmpty(pluginUrl.getUserInfo());
if (isAuthInfoSet && !isSecureProcotol) {
throw new IOException("Basic auth is only supported for HTTPS!");
}
terminal.println("Trying %s ...", pluginUrl.toExternalForm());
try {
downloadHelper.download(pluginUrl, pluginFile, progress, this.timeout);
downloaded = true;
terminal.println("Verifying %s checksums if available ...", pluginUrl.toExternalForm());
Tuple<URL, Path> sha1Info = pluginHandle.newChecksumUrlAndFile(environment, pluginUrl, "sha1");
verified = downloadHelper.downloadAndVerifyChecksum(sha1Info.v1(), pluginFile,
sha1Info.v2(), progress, this.timeout, HttpDownloadHelper.SHA1_CHECKSUM);
Tuple<URL, Path> md5Info = pluginHandle.newChecksumUrlAndFile(environment, pluginUrl, "md5");
verified = verified || downloadHelper.downloadAndVerifyChecksum(md5Info.v1(), pluginFile,
md5Info.v2(), progress, this.timeout, HttpDownloadHelper.MD5_CHECKSUM);
} catch (ElasticsearchTimeoutException | ElasticsearchCorruptionException e) {
throw e;
} catch (Exception e) {
// ignore
terminal.println("Failed: %s", ExceptionsHelper.detailedMessage(e));
}
} else {
if (PluginHandle.isOfficialPlugin(pluginHandle.name, pluginHandle.user, pluginHandle.version)) {
checkForOfficialPlugins(pluginHandle.name);
}
}
if (!downloaded && url == null) {
// We try all possible locations
for (URL url : pluginHandle.urls()) {
terminal.println("Trying %s ...", url.toExternalForm());
try {
downloadHelper.download(url, pluginFile, progress, this.timeout);
downloaded = true;
terminal.println("Verifying %s checksums if available ...", url.toExternalForm());
Tuple<URL, Path> sha1Info = pluginHandle.newChecksumUrlAndFile(environment, url, "sha1");
verified = downloadHelper.downloadAndVerifyChecksum(sha1Info.v1(), pluginFile,
sha1Info.v2(), progress, this.timeout, HttpDownloadHelper.SHA1_CHECKSUM);
Tuple<URL, Path> md5Info = pluginHandle.newChecksumUrlAndFile(environment, url, "md5");
verified = verified || downloadHelper.downloadAndVerifyChecksum(md5Info.v1(), pluginFile,
md5Info.v2(), progress, this.timeout, HttpDownloadHelper.MD5_CHECKSUM);
break;
} catch (ElasticsearchTimeoutException | ElasticsearchCorruptionException e) {
throw e;
} catch (Exception e) {
terminal.println(VERBOSE, "Failed: %s", ExceptionsHelper.detailedMessage(e));
}
}
}
if (!downloaded) {
// try to cleanup what we downloaded
IOUtils.deleteFilesIgnoringExceptions(pluginFile);
throw new IOException("failed to download out of all possible locations..., use --verbose to get detailed information");
}
if (verified == false) {
terminal.println("NOTE: Unable to verify checksum for downloaded plugin (unable to find .sha1 or .md5 file to verify)");
}
return pluginFile;
}
private void extract(PluginHandle pluginHandle, Terminal terminal, Path pluginFile, boolean batch) throws IOException {
// unzip plugin to a staging temp dir, named for the plugin
Path tmp = Files.createTempDirectory(environment.tmpFile(), null);
Path root = tmp.resolve(pluginHandle.name);
unzipPlugin(pluginFile, root);
// find the actual root (in case its unzipped with extra directory wrapping)
root = findPluginRoot(root);
// read and validate the plugin descriptor
PluginInfo info = PluginInfo.readFromProperties(root);
terminal.println(VERBOSE, "%s", info);
// don't let luser install plugin as a module...
// they might be unavoidably in maven central and are packaged up the same way)
if (MODULES.contains(info.getName())) {
throw new IOException("plugin '" + info.getName() + "' cannot be installed like this, it is a system module");
}
// update name in handle based on 'name' property found in descriptor file
pluginHandle = new PluginHandle(info.getName(), pluginHandle.version, pluginHandle.user);
final Path extractLocation = pluginHandle.extractedDir(environment);
if (Files.exists(extractLocation)) {
throw new IOException("plugin directory " + extractLocation.toAbsolutePath() + " already exists. To update the plugin, uninstall it first using 'remove " + pluginHandle.name + "' command");
}
// check for jar hell before any copying
jarHellCheck(root, info.isIsolated());
// read optional security policy (extra permissions)
// if it exists, confirm or warn the user
Path policy = root.resolve(PluginInfo.ES_PLUGIN_POLICY);
if (Files.exists(policy)) {
PluginSecurity.readPolicy(policy, terminal, environment, batch);
}
// install plugin
FileSystemUtils.copyDirectoryRecursively(root, extractLocation);
terminal.println("Installed %s into %s", pluginHandle.name, extractLocation.toAbsolutePath());
// cleanup
tryToDeletePath(terminal, tmp, pluginFile);
// take care of bin/ by moving and applying permissions if needed
Path sourcePluginBinDirectory = extractLocation.resolve("bin");
Path destPluginBinDirectory = pluginHandle.binDir(environment);
boolean needToCopyBinDirectory = Files.exists(sourcePluginBinDirectory);
if (needToCopyBinDirectory) {
if (Files.exists(destPluginBinDirectory) && !Files.isDirectory(destPluginBinDirectory)) {
tryToDeletePath(terminal, extractLocation);
throw new IOException("plugin bin directory " + destPluginBinDirectory + " is not a directory");
}
try {
copyBinDirectory(sourcePluginBinDirectory, destPluginBinDirectory, pluginHandle.name, terminal);
} catch (IOException e) {
// rollback and remove potentially before installed leftovers
terminal.printError("Error copying bin directory [%s] to [%s], cleaning up, reason: %s", sourcePluginBinDirectory, destPluginBinDirectory, ExceptionsHelper.detailedMessage(e));
tryToDeletePath(terminal, extractLocation, pluginHandle.binDir(environment));
throw e;
}
}
Path sourceConfigDirectory = extractLocation.resolve("config");
Path destConfigDirectory = pluginHandle.configDir(environment);
boolean needToCopyConfigDirectory = Files.exists(sourceConfigDirectory);
if (needToCopyConfigDirectory) {
if (Files.exists(destConfigDirectory) && !Files.isDirectory(destConfigDirectory)) {
tryToDeletePath(terminal, extractLocation, destPluginBinDirectory);
throw new IOException("plugin config directory " + destConfigDirectory + " is not a directory");
}
try {
terminal.println(VERBOSE, "Found config, moving to %s", destConfigDirectory.toAbsolutePath());
moveFilesWithoutOverwriting(sourceConfigDirectory, destConfigDirectory, ".new");
if (Environment.getFileStore(destConfigDirectory).supportsFileAttributeView(PosixFileAttributeView.class)) {
//We copy owner, group and permissions from the parent ES_CONFIG directory, assuming they were properly set depending
// on how es was installed in the first place: can be root:elasticsearch (750) if es was installed from rpm/deb packages
// or most likely elasticsearch:elasticsearch if installed from tar/zip. As for permissions we don't rely on umask.
PosixFileAttributes parentDirAttributes = Files.getFileAttributeView(destConfigDirectory.getParent(), PosixFileAttributeView.class).readAttributes();
//for files though, we make sure not to copy execute permissions from the parent dir and leave them untouched
Set<PosixFilePermission> baseFilePermissions = new HashSet<>();
for (PosixFilePermission posixFilePermission : parentDirAttributes.permissions()) {
switch (posixFilePermission) {
case OWNER_EXECUTE:
case GROUP_EXECUTE:
case OTHERS_EXECUTE:
break;
default:
baseFilePermissions.add(posixFilePermission);
}
}
Files.walkFileTree(destConfigDirectory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (attrs.isRegularFile()) {
Set<PosixFilePermission> newFilePermissions = new HashSet<>(baseFilePermissions);
Set<PosixFilePermission> currentFilePermissions = Files.getPosixFilePermissions(file);
for (PosixFilePermission posixFilePermission : currentFilePermissions) {
switch (posixFilePermission) {
case OWNER_EXECUTE:
case GROUP_EXECUTE:
case OTHERS_EXECUTE:
newFilePermissions.add(posixFilePermission);
}
}
setPosixFileAttributes(file, parentDirAttributes.owner(), parentDirAttributes.group(), newFilePermissions);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
setPosixFileAttributes(dir, parentDirAttributes.owner(), parentDirAttributes.group(), parentDirAttributes.permissions());
return FileVisitResult.CONTINUE;
}
});
} else {
terminal.println(VERBOSE, "Skipping posix permissions - filestore doesn't support posix permission");
}
terminal.println(VERBOSE, "Installed %s into %s", pluginHandle.name, destConfigDirectory.toAbsolutePath());
} catch (IOException e) {
terminal.printError("Error copying config directory [%s] to [%s], cleaning up, reason: %s", sourceConfigDirectory, destConfigDirectory, ExceptionsHelper.detailedMessage(e));
tryToDeletePath(terminal, extractLocation, destPluginBinDirectory, destConfigDirectory);
throw e;
}
}
}
private static void setPosixFileAttributes(Path path, UserPrincipal owner, GroupPrincipal group, Set<PosixFilePermission> permissions) throws IOException {
PosixFileAttributeView fileAttributeView = Files.getFileAttributeView(path, PosixFileAttributeView.class);
fileAttributeView.setOwner(owner);
fileAttributeView.setGroup(group);
fileAttributeView.setPermissions(permissions);
}
static void tryToDeletePath(Terminal terminal, Path ... paths) {
for (Path path : paths) {
try {
IOUtils.rm(path);
} catch (IOException e) {
terminal.printError(e);
}
}
}
private void copyBinDirectory(Path sourcePluginBinDirectory, Path destPluginBinDirectory, String pluginName, Terminal terminal) throws IOException {
boolean canCopyFromSource = Files.exists(sourcePluginBinDirectory) && Files.isReadable(sourcePluginBinDirectory) && Files.isDirectory(sourcePluginBinDirectory);
if (canCopyFromSource) {
terminal.println(VERBOSE, "Found bin, moving to %s", destPluginBinDirectory.toAbsolutePath());
if (Files.exists(destPluginBinDirectory)) {
IOUtils.rm(destPluginBinDirectory);
}
try {
Files.createDirectories(destPluginBinDirectory.getParent());
FileSystemUtils.move(sourcePluginBinDirectory, destPluginBinDirectory);
} catch (IOException e) {
throw new IOException("Could not move [" + sourcePluginBinDirectory + "] to [" + destPluginBinDirectory + "]", e);
}
if (Environment.getFileStore(destPluginBinDirectory).supportsFileAttributeView(PosixFileAttributeView.class)) {
PosixFileAttributes parentDirAttributes = Files.getFileAttributeView(destPluginBinDirectory.getParent(), PosixFileAttributeView.class).readAttributes();
//copy permissions from parent bin directory
Set<PosixFilePermission> filePermissions = new HashSet<>();
for (PosixFilePermission posixFilePermission : parentDirAttributes.permissions()) {
switch (posixFilePermission) {
case OWNER_EXECUTE:
case GROUP_EXECUTE:
case OTHERS_EXECUTE:
break;
default:
filePermissions.add(posixFilePermission);
}
}
// add file execute permissions to existing perms, so execution will work.
filePermissions.add(PosixFilePermission.OWNER_EXECUTE);
filePermissions.add(PosixFilePermission.GROUP_EXECUTE);
filePermissions.add(PosixFilePermission.OTHERS_EXECUTE);
Files.walkFileTree(destPluginBinDirectory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (attrs.isRegularFile()) {
setPosixFileAttributes(file, parentDirAttributes.owner(), parentDirAttributes.group(), filePermissions);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
setPosixFileAttributes(dir, parentDirAttributes.owner(), parentDirAttributes.group(), parentDirAttributes.permissions());
return FileVisitResult.CONTINUE;
}
});
} else {
terminal.println(VERBOSE, "Skipping posix permissions - filestore doesn't support posix permission");
}
terminal.println(VERBOSE, "Installed %s into %s", pluginName, destPluginBinDirectory.toAbsolutePath());
}
}
/** we check whether we need to remove the top-level folder while extracting
* sometimes (e.g. github) the downloaded archive contains a top-level folder which needs to be removed
*/
private Path findPluginRoot(Path dir) throws IOException {
if (Files.exists(dir.resolve(PluginInfo.ES_PLUGIN_PROPERTIES))) {
return dir;
} else {
final Path[] topLevelFiles = FileSystemUtils.files(dir);
if (topLevelFiles.length == 1 && Files.isDirectory(topLevelFiles[0])) {
Path subdir = topLevelFiles[0];
if (Files.exists(subdir.resolve(PluginInfo.ES_PLUGIN_PROPERTIES))) {
return subdir;
}
}
}
throw new RuntimeException("Could not find plugin descriptor '" + PluginInfo.ES_PLUGIN_PROPERTIES + "' in plugin zip");
}
/** check a candidate plugin for jar hell before installing it */
private void jarHellCheck(Path candidate, boolean isolated) throws IOException {
// create list of current jars in classpath
final List<URL> jars = new ArrayList<>();
jars.addAll(Arrays.asList(JarHell.parseClassPath()));
// read existing bundles. this does some checks on the installation too.
List<Bundle> bundles = PluginsService.getPluginBundles(environment.pluginsFile());
// if we aren't isolated, we need to jarhellcheck against any other non-isolated plugins
// thats always the first bundle
if (isolated == false) {
jars.addAll(bundles.get(0).urls);
}
// add plugin jars to the list
Path pluginJars[] = FileSystemUtils.files(candidate, "*.jar");
for (Path jar : pluginJars) {
jars.add(jar.toUri().toURL());
}
// check combined (current classpath + new jars to-be-added)
try {
JarHell.checkJarHell(jars.toArray(new URL[jars.size()]));
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
private void unzipPlugin(Path zip, Path target) throws IOException {
Files.createDirectories(target);
try (ZipInputStream zipInput = new ZipInputStream(Files.newInputStream(zip))) {
ZipEntry entry;
byte[] buffer = new byte[8192];
while ((entry = zipInput.getNextEntry()) != null) {
Path targetFile = target.resolve(entry.getName());
// be on the safe side: do not rely on that directories are always extracted
// before their children (although this makes sense, but is it guaranteed?)
Files.createDirectories(targetFile.getParent());
if (entry.isDirectory() == false) {
try (OutputStream out = Files.newOutputStream(targetFile)) {
int len;
while((len = zipInput.read(buffer)) >= 0) {
out.write(buffer, 0, len);
}
}
}
zipInput.closeEntry();
}
}
}
public void removePlugin(String name, Terminal terminal) throws IOException {
if (name == null) {
throw new IllegalArgumentException("plugin name must be supplied with remove [name].");
}
PluginHandle pluginHandle = PluginHandle.parse(name);
boolean removed = false;
checkForForbiddenName(pluginHandle.name);
Path pluginToDelete = pluginHandle.extractedDir(environment);
if (Files.exists(pluginToDelete)) {
terminal.println(VERBOSE, "Removing: %s", pluginToDelete);
try {
IOUtils.rm(pluginToDelete);
} catch (IOException ex){
throw new IOException("Unable to remove " + pluginHandle.name + ". Check file permissions on " +
pluginToDelete.toString(), ex);
}
removed = true;
}
Path binLocation = pluginHandle.binDir(environment);
if (Files.exists(binLocation)) {
terminal.println(VERBOSE, "Removing: %s", binLocation);
try {
IOUtils.rm(binLocation);
} catch (IOException ex){
throw new IOException("Unable to remove " + pluginHandle.name + ". Check file permissions on " +
binLocation.toString(), ex);
}
removed = true;
}
if (removed) {
terminal.println("Removed %s", name);
} else {
terminal.println("Plugin %s not found. Run \"plugin list\" to get list of installed plugins.", name);
}
}
static void checkForForbiddenName(String name) {
if (!hasLength(name) || BLACKLIST.contains(name.toLowerCase(Locale.ROOT))) {
throw new IllegalArgumentException("Illegal plugin name: " + name);
}
}
protected static void checkForOfficialPlugins(String name) {
// We make sure that users can use only new short naming for official plugins only
if (!OFFICIAL_PLUGINS.contains(name)) {
throw new IllegalArgumentException(name +
" is not an official plugin so you should install it using elasticsearch/" +
name + "/latest naming form.");
}
}
public Path[] getListInstalledPlugins() throws IOException {
if (!Files.exists(environment.pluginsFile())) {
return new Path[0];
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(environment.pluginsFile())) {
return StreamSupport.stream(stream.spliterator(), false).toArray(length -> new Path[length]);
}
}
public void listInstalledPlugins(Terminal terminal) throws IOException {
Path[] plugins = getListInstalledPlugins();
terminal.println("Installed plugins in %s:", environment.pluginsFile().toAbsolutePath());
if (plugins == null || plugins.length == 0) {
terminal.println(" - No plugin detected");
} else {
for (Path plugin : plugins) {
terminal.println(" - " + plugin.getFileName());
}
}
}
/**
* Helper class to extract properly user name, repository name, version and plugin name
* from plugin name given by a user.
*/
static class PluginHandle {
final String version;
final String user;
final String name;
PluginHandle(String name, String version, String user) {
this.version = version;
this.user = user;
this.name = name;
}
List<URL> urls() {
List<URL> urls = new ArrayList<>();
if (version != null) {
// Elasticsearch new download service uses groupId org.elasticsearch.plugin from 2.0.0
if (user == null) {
if (!Strings.isNullOrEmpty(System.getProperty(PROPERTY_SUPPORT_STAGING_URLS))) {
addUrl(urls, String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/staging/%s-%s/org/elasticsearch/plugin/%s/%s/%s-%s.zip", version, Build.CURRENT.shortHash(), name, version, name, version));
}
addUrl(urls, String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/release/org/elasticsearch/plugin/%s/%s/%s-%s.zip", name, version, name, version));
} else {
// Elasticsearch old download service
addUrl(urls, String.format(Locale.ROOT, "https://download.elastic.co/%1$s/%2$s/%2$s-%3$s.zip", user, name, version));
// Maven central repository
addUrl(urls, String.format(Locale.ROOT, "https://search.maven.org/remotecontent?filepath=%1$s/%2$s/%3$s/%2$s-%3$s.zip", user.replace('.', '/'), name, version));
// Sonatype repository
addUrl(urls, String.format(Locale.ROOT, "https://oss.sonatype.org/service/local/repositories/releases/content/%1$s/%2$s/%3$s/%2$s-%3$s.zip", user.replace('.', '/'), name, version));
// Github repository
addUrl(urls, String.format(Locale.ROOT, "https://github.com/%1$s/%2$s/archive/%3$s.zip", user, name, version));
}
}
if (user != null) {
// Github repository for master branch (assume site)
addUrl(urls, String.format(Locale.ROOT, "https://github.com/%1$s/%2$s/archive/master.zip", user, name));
}
return urls;
}
private static void addUrl(List<URL> urls, String url) {
try {
urls.add(new URL(url));
} catch (MalformedURLException e) {
// We simply ignore malformed URL
}
}
Path newDistroFile(Environment env) throws IOException {
return Files.createTempFile(env.tmpFile(), name, ".zip");
}
Tuple<URL, Path> newChecksumUrlAndFile(Environment env, URL originalUrl, String suffix) throws IOException {
URL newUrl = new URL(originalUrl.toString() + "." + suffix);
return new Tuple<>(newUrl, Files.createTempFile(env.tmpFile(), name, ".zip." + suffix));
}
Path extractedDir(Environment env) {
return env.pluginsFile().resolve(name);
}
Path binDir(Environment env) {
return env.binFile().resolve(name);
}
Path configDir(Environment env) {
return env.configFile().resolve(name);
}
static PluginHandle parse(String name) {
String[] elements = name.split("/");
// We first consider the simplest form: pluginname
String repo = elements[0];
String user = null;
String version = null;
// We consider the form: username/pluginname
if (elements.length > 1) {
user = elements[0];
repo = elements[1];
// We consider the form: username/pluginname/version
if (elements.length > 2) {
version = elements[2];
}
}
if (isOfficialPlugin(repo, user, version)) {
return new PluginHandle(repo, Version.CURRENT.number(), null);
}
return new PluginHandle(repo, version, user);
}
static boolean isOfficialPlugin(String repo, String user, String version) {
return version == null && user == null && !Strings.isNullOrEmpty(repo);
}
}
}

View File

@ -1,256 +0,0 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import org.apache.commons.cli.CommandLine;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.cli.CliTool;
import org.elasticsearch.common.cli.CliToolConfig;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.logging.log4j.LogConfigurator;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.env.Environment;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import org.elasticsearch.plugins.PluginManager.OutputMode;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.Locale;
import static org.elasticsearch.common.cli.CliToolConfig.Builder.cmd;
import static org.elasticsearch.common.cli.CliToolConfig.Builder.option;
public class PluginManagerCliParser extends CliTool {
// By default timeout is 0 which means no timeout
public static final TimeValue DEFAULT_TIMEOUT = TimeValue.timeValueMillis(0);
private static final CliToolConfig CONFIG = CliToolConfig.config("plugin", PluginManagerCliParser.class)
.cmds(ListPlugins.CMD, Install.CMD, Remove.CMD)
.build();
public static void main(String[] args) {
// initialize default for es.logger.level because we will not read the logging.yml
String loggerLevel = System.getProperty("es.logger.level", "INFO");
// Set the appender for all potential log files to terminal so that other components that use the logger print out the
// same terminal.
// The reason for this is that the plugin cli cannot be configured with a file appender because when the plugin command is
// executed there is no way of knowing where the logfiles should be placed. For example, if elasticsearch
// is run as service then the logs should be at /var/log/elasticsearch but when started from the tar they should be at es.home/logs.
// Therefore we print to Terminal.
Environment env = InternalSettingsPreparer.prepareEnvironment(Settings.builder()
.put("appender.terminal.type", "terminal")
.put("rootLogger", "${es.logger.level}, terminal")
.put("es.logger.level", loggerLevel)
.build(), Terminal.DEFAULT);
// configure but do not read the logging conf file
LogConfigurator.configure(env.settings(), false);
int status = new PluginManagerCliParser().execute(args).status();
exit(status);
}
@SuppressForbidden(reason = "Allowed to exit explicitly from #main()")
private static void exit(int status) {
System.exit(status);
}
public PluginManagerCliParser() {
super(CONFIG);
}
public PluginManagerCliParser(Terminal terminal) {
super(CONFIG, terminal);
}
@Override
protected Command parse(String cmdName, CommandLine cli) throws Exception {
switch (cmdName.toLowerCase(Locale.ROOT)) {
case Install.NAME:
return Install.parse(terminal, cli);
case ListPlugins.NAME:
return ListPlugins.parse(terminal, cli);
case Remove.NAME:
return Remove.parse(terminal, cli);
default:
assert false : "can't get here as cmd name is validated before this method is called";
return exitCmd(ExitStatus.USAGE);
}
}
/**
* List all installed plugins
*/
static class ListPlugins extends CliTool.Command {
private static final String NAME = "list";
private static final CliToolConfig.Cmd CMD = cmd(NAME, ListPlugins.class).build();
private final OutputMode outputMode;
public static Command parse(Terminal terminal, CommandLine cli) {
OutputMode outputMode = OutputMode.DEFAULT;
if (cli.hasOption("s")) {
outputMode = OutputMode.SILENT;
}
if (cli.hasOption("v")) {
outputMode = OutputMode.VERBOSE;
}
return new ListPlugins(terminal, outputMode);
}
ListPlugins(Terminal terminal, OutputMode outputMode) {
super(terminal);
this.outputMode = outputMode;
}
@Override
public ExitStatus execute(Settings settings, Environment env) throws Exception {
PluginManager pluginManager = new PluginManager(env, null, outputMode, DEFAULT_TIMEOUT);
pluginManager.listInstalledPlugins(terminal);
return ExitStatus.OK;
}
}
/**
* Remove a plugin
*/
static class Remove extends CliTool.Command {
private static final String NAME = "remove";
private static final CliToolConfig.Cmd CMD = cmd(NAME, Remove.class).build();
public static Command parse(Terminal terminal, CommandLine cli) {
String[] args = cli.getArgs();
if (args.length == 0) {
return exitCmd(ExitStatus.USAGE, terminal, "plugin name is missing (type -h for help)");
}
OutputMode outputMode = OutputMode.DEFAULT;
if (cli.hasOption("s")) {
outputMode = OutputMode.SILENT;
}
if (cli.hasOption("v")) {
outputMode = OutputMode.VERBOSE;
}
return new Remove(terminal, outputMode, args[0]);
}
private OutputMode outputMode;
final String pluginName;
Remove(Terminal terminal, OutputMode outputMode, String pluginToRemove) {
super(terminal);
this.outputMode = outputMode;
this.pluginName = pluginToRemove;
}
@Override
public ExitStatus execute(Settings settings, Environment env) throws Exception {
PluginManager pluginManager = new PluginManager(env, null, outputMode, DEFAULT_TIMEOUT);
terminal.println("-> Removing " + Strings.coalesceToEmpty(pluginName) + "...");
pluginManager.removePlugin(pluginName, terminal);
return ExitStatus.OK;
}
}
/**
* Installs a plugin
*/
static class Install extends Command {
private static final String NAME = "install";
private static final CliToolConfig.Cmd CMD = cmd(NAME, Install.class)
.options(option("t", "timeout").required(false).hasArg(false))
.options(option("b", "batch").required(false))
.build();
static Command parse(Terminal terminal, CommandLine cli) {
String[] args = cli.getArgs();
// install [plugin-name/url]
if ((args == null) || (args.length == 0)) {
return exitCmd(ExitStatus.USAGE, terminal, "plugin name or url is missing (type -h for help)");
}
String name = args[0];
URL optionalPluginUrl = null;
// try parsing cli argument as URL
try {
optionalPluginUrl = new URL(name);
name = null;
} catch (MalformedURLException e) {
// we tried to parse the cli argument as url and failed
// continue treating it as a symbolic plugin name like `analysis-icu` etc.
}
TimeValue timeout = TimeValue.parseTimeValue(cli.getOptionValue("t"), DEFAULT_TIMEOUT, "cli");
OutputMode outputMode = OutputMode.DEFAULT;
if (cli.hasOption("s")) {
outputMode = OutputMode.SILENT;
}
if (cli.hasOption("v")) {
outputMode = OutputMode.VERBOSE;
}
boolean batch = System.console() == null;
if (cli.hasOption("b")) {
batch = true;
}
return new Install(terminal, name, outputMode, optionalPluginUrl, timeout, batch);
}
final String name;
private OutputMode outputMode;
final URL url;
final TimeValue timeout;
final boolean batch;
Install(Terminal terminal, String name, OutputMode outputMode, URL url, TimeValue timeout, boolean batch) {
super(terminal);
this.name = name;
this.outputMode = outputMode;
this.url = url;
this.timeout = timeout;
this.batch = batch;
}
@Override
public ExitStatus execute(Settings settings, Environment env) throws Exception {
PluginManager pluginManager = new PluginManager(env, url, outputMode, timeout);
if (name != null) {
terminal.println("-> Installing " + Strings.coalesceToEmpty(name) + "...");
} else {
terminal.println("-> Installing from " + URLDecoder.decode(url.toString(), "UTF-8") + "...");
}
pluginManager.downloadAndExtract(name, terminal, batch);
return ExitStatus.OK;
}
}
}

View File

@ -19,6 +19,7 @@
package org.elasticsearch.plugins;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.cli.Terminal.Verbosity;
import org.elasticsearch.env.Environment;
@ -38,7 +39,7 @@ import java.util.Comparator;
import java.util.List;
class PluginSecurity {
/**
* Reads plugin policy, prints/confirms exceptions
*/
@ -49,7 +50,7 @@ class PluginSecurity {
terminal.print(Verbosity.VERBOSE, "plugin has a policy file with no additional permissions");
return;
}
// sort permissions in a reasonable order
Collections.sort(requested, new Comparator<Permission>() {
@Override
@ -80,7 +81,7 @@ class PluginSecurity {
return cmp;
}
});
terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
terminal.println(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @");
terminal.println(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
@ -98,11 +99,11 @@ class PluginSecurity {
}
}
}
/** Format permission type, name, and actions into a string */
static String formatPermission(Permission permission) {
StringBuilder sb = new StringBuilder();
String clazz = null;
if (permission instanceof UnresolvedPermission) {
clazz = ((UnresolvedPermission) permission).getUnresolvedType();
@ -110,7 +111,7 @@ class PluginSecurity {
clazz = permission.getClass().getName();
}
sb.append(clazz);
String name = null;
if (permission instanceof UnresolvedPermission) {
name = ((UnresolvedPermission) permission).getUnresolvedName();
@ -121,7 +122,7 @@ class PluginSecurity {
sb.append(' ');
sb.append(name);
}
String actions = null;
if (permission instanceof UnresolvedPermission) {
actions = ((UnresolvedPermission) permission).getUnresolvedActions();
@ -134,7 +135,7 @@ class PluginSecurity {
}
return sb.toString();
}
/**
* Parses plugin policy into a set of permissions
*/
@ -151,8 +152,8 @@ class PluginSecurity {
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
PluginManager.tryToDeletePath(terminal, emptyPolicyFile);
IOUtils.rm(emptyPolicyFile);
// parse the plugin's policy file into a set of permissions
final Policy policy;
try {

View File

@ -0,0 +1,77 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
import org.apache.lucene.util.IOUtils;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.cli.CliTool;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import static org.elasticsearch.common.cli.Terminal.Verbosity.VERBOSE;
/**
* A command for the plugin cli to remove a plugin from elasticsearch.
*/
class RemovePluginCommand extends CliTool.Command {
private final String pluginName;
public RemovePluginCommand(Terminal terminal, String pluginName) {
super(terminal);
this.pluginName = pluginName;
}
@Override
public CliTool.ExitStatus execute(Settings settings, Environment env) throws Exception {
terminal.println("-> Removing " + Strings.coalesceToEmpty(pluginName) + "...");
Path pluginDir = env.pluginsFile().resolve(pluginName);
if (Files.exists(pluginDir) == false) {
throw new IllegalArgumentException("Plugin " + pluginName + " not found. Run 'plugin list' to get list of installed plugins.");
}
List<Path> pluginPaths = new ArrayList<>();
Path pluginBinDir = env.binFile().resolve(pluginName);
if (Files.exists(pluginBinDir)) {
if (Files.isDirectory(pluginBinDir) == false) {
throw new IllegalStateException("Bin dir for " + pluginName + " is not a directory");
}
pluginPaths.add(pluginBinDir);
terminal.println(VERBOSE, "Removing: %s", pluginBinDir);
}
terminal.println(VERBOSE, "Removing: %s", pluginDir);
Path tmpPluginDir = env.pluginsFile().resolve(".removing-" + pluginName);
Files.move(pluginDir, tmpPluginDir, StandardCopyOption.ATOMIC_MOVE);
pluginPaths.add(tmpPluginDir);
IOUtils.rm(pluginPaths.toArray(new Path[pluginPaths.size()]));
return CliTool.ExitStatus.OK;
}
}

View File

@ -13,16 +13,11 @@ DESCRIPTION
Officially supported or commercial plugins require just the plugin name:
plugin install analysis-icu
plugin install shield
plugin install x-pack
Plugins from GitHub require 'username/repository' or 'username/repository/version':
Plugins from Maven Central require 'groupId:artifactId:version':
plugin install lmenezes/elasticsearch-kopf
plugin install lmenezes/elasticsearch-kopf/1.5.7
Plugins from Maven Central or Sonatype require 'groupId/artifactId/version':
plugin install org.elasticsearch/elasticsearch-mapper-attachments/2.6.0
plugin install org.elasticsearch:mapper-attachments:3.0.0
Plugins can be installed from a custom URL or file location as follows:
@ -58,8 +53,6 @@ OFFICIAL PLUGINS
OPTIONS
-t,--timeout Timeout until the plugin download is abort
-v,--verbose Verbose output
-h,--help Shows this message

View File

@ -48,91 +48,6 @@ public class FileSystemUtilsTests extends ESTestCase {
dst = createTempDir();
Files.createDirectories(src);
Files.createDirectories(dst);
// We first copy sources test files from src/test/resources
// Because after when the test runs, src files are moved to their destination
final Path path = getDataPath("/org/elasticsearch/common/io/copyappend");
FileSystemUtils.copyDirectoryRecursively(path, src);
}
public void testMoveOverExistingFileAndAppend() throws IOException {
FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v1"), dst, ".new");
assertFileContent(dst, "file1.txt", "version1");
assertFileContent(dst, "dir/file2.txt", "version1");
FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v2"), dst, ".new");
assertFileContent(dst, "file1.txt", "version1");
assertFileContent(dst, "dir/file2.txt", "version1");
assertFileContent(dst, "file1.txt.new", "version2");
assertFileContent(dst, "dir/file2.txt.new", "version2");
assertFileContent(dst, "file3.txt", "version1");
assertFileContent(dst, "dir/subdir/file4.txt", "version1");
FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v3"), dst, ".new");
assertFileContent(dst, "file1.txt", "version1");
assertFileContent(dst, "dir/file2.txt", "version1");
assertFileContent(dst, "file1.txt.new", "version3");
assertFileContent(dst, "dir/file2.txt.new", "version3");
assertFileContent(dst, "file3.txt", "version1");
assertFileContent(dst, "dir/subdir/file4.txt", "version1");
assertFileContent(dst, "file3.txt.new", "version2");
assertFileContent(dst, "dir/subdir/file4.txt.new", "version2");
assertFileContent(dst, "dir/subdir/file5.txt", "version1");
}
public void testMoveOverExistingFileAndIgnore() throws IOException {
Path dest = createTempDir();
FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v1"), dest, null);
assertFileContent(dest, "file1.txt", "version1");
assertFileContent(dest, "dir/file2.txt", "version1");
FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v2"), dest, null);
assertFileContent(dest, "file1.txt", "version1");
assertFileContent(dest, "dir/file2.txt", "version1");
assertFileContent(dest, "file1.txt.new", null);
assertFileContent(dest, "dir/file2.txt.new", null);
assertFileContent(dest, "file3.txt", "version1");
assertFileContent(dest, "dir/subdir/file4.txt", "version1");
FileSystemUtils.moveFilesWithoutOverwriting(src.resolve("v3"), dest, null);
assertFileContent(dest, "file1.txt", "version1");
assertFileContent(dest, "dir/file2.txt", "version1");
assertFileContent(dest, "file1.txt.new", null);
assertFileContent(dest, "dir/file2.txt.new", null);
assertFileContent(dest, "file3.txt", "version1");
assertFileContent(dest, "dir/subdir/file4.txt", "version1");
assertFileContent(dest, "file3.txt.new", null);
assertFileContent(dest, "dir/subdir/file4.txt.new", null);
assertFileContent(dest, "dir/subdir/file5.txt", "version1");
}
public void testMoveFilesDoesNotCreateSameFileWithSuffix() throws Exception {
Path[] dirs = new Path[] { createTempDir(), createTempDir(), createTempDir()};
for (Path dir : dirs) {
Files.write(dir.resolve("file1.txt"), "file1".getBytes(StandardCharsets.UTF_8));
Files.createDirectory(dir.resolve("dir"));
Files.write(dir.resolve("dir").resolve("file2.txt"), "file2".getBytes(StandardCharsets.UTF_8));
}
FileSystemUtils.moveFilesWithoutOverwriting(dirs[0], dst, ".new");
assertFileContent(dst, "file1.txt", "file1");
assertFileContent(dst, "dir/file2.txt", "file2");
// do the same operation again, make sure, no .new files have been added
FileSystemUtils.moveFilesWithoutOverwriting(dirs[1], dst, ".new");
assertFileContent(dst, "file1.txt", "file1");
assertFileContent(dst, "dir/file2.txt", "file2");
assertFileNotExists(dst.resolve("file1.txt.new"));
assertFileNotExists(dst.resolve("dir").resolve("file2.txt.new"));
// change file content, make sure it gets updated
Files.write(dirs[2].resolve("dir").resolve("file2.txt"), "UPDATED".getBytes(StandardCharsets.UTF_8));
FileSystemUtils.moveFilesWithoutOverwriting(dirs[2], dst, ".new");
assertFileContent(dst, "file1.txt", "file1");
assertFileContent(dst, "dir/file2.txt", "file2");
assertFileContent(dst, "dir/file2.txt.new", "UPDATED");
}
public void testAppend() {

View File

@ -32,25 +32,25 @@ import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
public class PluginManagerCliTests extends CliToolTestCase {
public class PluginCliTests extends CliToolTestCase {
public void testHelpWorks() throws IOException {
CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal();
assertThat(new PluginManagerCliParser(terminal).execute(args("--help")), is(OK_AND_EXIT));
assertThat(new PluginCli(terminal).execute(args("--help")), is(OK_AND_EXIT));
assertTerminalOutputContainsHelpFile(terminal, "/org/elasticsearch/plugins/plugin.help");
terminal.getTerminalOutput().clear();
assertThat(new PluginManagerCliParser(terminal).execute(args("install -h")), is(OK_AND_EXIT));
assertThat(new PluginCli(terminal).execute(args("install -h")), is(OK_AND_EXIT));
assertTerminalOutputContainsHelpFile(terminal, "/org/elasticsearch/plugins/plugin-install.help");
for (String plugin : PluginManager.OFFICIAL_PLUGINS) {
for (String plugin : InstallPluginCommand.OFFICIAL_PLUGINS) {
assertThat(terminal.getTerminalOutput(), hasItem(containsString(plugin)));
}
terminal.getTerminalOutput().clear();
assertThat(new PluginManagerCliParser(terminal).execute(args("remove --help")), is(OK_AND_EXIT));
assertThat(new PluginCli(terminal).execute(args("remove --help")), is(OK_AND_EXIT));
assertTerminalOutputContainsHelpFile(terminal, "/org/elasticsearch/plugins/plugin-remove.help");
terminal.getTerminalOutput().clear();
assertThat(new PluginManagerCliParser(terminal).execute(args("list -h")), is(OK_AND_EXIT));
assertThat(new PluginCli(terminal).execute(args("list -h")), is(OK_AND_EXIT));
assertTerminalOutputContainsHelpFile(terminal, "/org/elasticsearch/plugins/plugin-list.help");
}
@ -58,8 +58,7 @@ public class PluginManagerCliTests extends CliToolTestCase {
CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal();
Path tmpDir = createTempDir().resolve("foo deps");
String finalDir = tmpDir.toAbsolutePath().toUri().toURL().toString();
logger.warn(finalDir);
CliTool.ExitStatus execute = new PluginManagerCliParser(terminal).execute(args("install " + finalDir));
CliTool.ExitStatus execute = new PluginCli(terminal).execute("install", finalDir);
assertThat(execute.status(), is(IO_ERROR.status()));
}
}

View File

@ -1,35 +0,0 @@
/*
* 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.
*/
package org.elasticsearch.plugins.loading.classpath;
import org.elasticsearch.plugins.Plugin;
public class InClassPathPlugin extends Plugin {
@Override
public String name() {
return "in-classpath-plugin";
}
@Override
public String description() {
return "A plugin defined in class path";
}
}

View File

@ -1,19 +0,0 @@
################################################################
# 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.
################################################################
plugin=org.elasticsearch.plugins.loading.classpath.InClassPathPlugin

View File

@ -110,4 +110,4 @@ fi
HOSTNAME=`hostname | cut -d. -f1`
export HOSTNAME
eval "$JAVA" -client -Delasticsearch -Des.path.home="\"$ES_HOME\"" $properties -cp "\"$ES_HOME/lib/*\"" org.elasticsearch.plugins.PluginManagerCliParser $args
eval "$JAVA" -client -Delasticsearch -Des.path.home="\"$ES_HOME\"" $properties -cp "\"$ES_HOME/lib/*\"" org.elasticsearch.plugins.PluginCli $args

View File

@ -0,0 +1,442 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.apache.lucene.util.LuceneTestCase;
import org.elasticsearch.Version;
import org.elasticsearch.common.cli.CliTool;
import org.elasticsearch.common.cli.CliToolTestCase;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.junit.BeforeClass;
@LuceneTestCase.SuppressFileSystems("*")
public class InstallPluginCommandTests extends ESTestCase {
private static boolean isPosix;
@BeforeClass
public static void checkPosix() throws IOException {
isPosix = Files.getFileAttributeView(createTempFile(), PosixFileAttributeView.class) != null;
}
/** Stores the posix attributes for a path and resets them on close. */
static class PosixPermissionsResetter implements AutoCloseable {
private final PosixFileAttributeView attributeView;
final Set<PosixFilePermission> permissions;
public PosixPermissionsResetter(Path path) throws IOException {
attributeView = Files.getFileAttributeView(path, PosixFileAttributeView.class);
assertNotNull(attributeView);
permissions = attributeView.readAttributes().permissions();
}
@Override
public void close() throws IOException {
attributeView.setPermissions(permissions);
}
public void setPermissions(Set<PosixFilePermission> newPermissions) throws IOException {
attributeView.setPermissions(newPermissions);
}
}
/** Creates a test environment with bin, config and plugins directories. */
static Environment createEnv() throws IOException {
Path home = createTempDir();
Files.createDirectories(home.resolve("bin"));
Files.createFile(home.resolve("bin").resolve("elasticsearch"));
Files.createDirectories(home.resolve("config"));
Files.createFile(home.resolve("config").resolve("elasticsearch.yml"));
Files.createDirectories(home.resolve("plugins"));
Settings settings = Settings.builder()
.put("path.home", home)
.build();
return new Environment(settings);
}
/** creates a fake jar file with empty class files */
static void writeJar(Path jar, String... classes) throws IOException {
try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(jar))) {
for (String clazz : classes) {
stream.putNextEntry(new ZipEntry(clazz + ".class")); // no package names, just support simple classes
}
}
}
static String writeZip(Path structure) throws IOException {
Path zip = createTempDir().resolve(structure.getFileName() + ".zip");
try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zip))) {
Files.walkFileTree(structure, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
stream.putNextEntry(new ZipEntry(structure.relativize(file).toString()));
Files.copy(file, stream);
return FileVisitResult.CONTINUE;
}
});
}
return zip.toUri().toURL().toString();
}
/** creates a plugin .zip and returns the url for testing */
static String createPlugin(String name, Path structure) throws IOException {
PluginTestUtil.writeProperties(structure,
"description", "fake desc",
"name", name,
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin");
writeJar(structure.resolve("plugin.jar"), "FakePlugin");
return writeZip(structure);
}
static CliToolTestCase.CaptureOutputTerminal installPlugin(String pluginUrl, Environment env) throws Exception {
CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal(Terminal.Verbosity.NORMAL);
CliTool.ExitStatus status = new InstallPluginCommand(terminal, pluginUrl, true).execute(env.settings(), env);
assertEquals(CliTool.ExitStatus.OK, status);
return terminal;
}
void assertPlugin(String name, Path original, Environment env) throws IOException {
Path got = env.pluginsFile().resolve(name);
assertTrue("dir " + name + " exists", Files.exists(got));
assertTrue("jar was copied", Files.exists(got.resolve("plugin.jar")));
assertFalse("bin was not copied", Files.exists(got.resolve("bin")));
assertFalse("config was not copied", Files.exists(got.resolve("config")));
if (Files.exists(original.resolve("bin"))) {
Path binDir = env.binFile().resolve(name);
assertTrue("bin dir exists", Files.exists(binDir));
assertTrue("bin is a dir", Files.isDirectory(binDir));
PosixFileAttributes binAttributes = null;
if (isPosix) {
binAttributes = Files.readAttributes(env.binFile(), PosixFileAttributes.class);
}
try (DirectoryStream<Path> stream = Files.newDirectoryStream(binDir)) {
for (Path file : stream) {
assertFalse("not a dir", Files.isDirectory(file));
if (isPosix) {
PosixFileAttributes attributes = Files.readAttributes(file, PosixFileAttributes.class);
Set<PosixFilePermission> expectedPermissions = new HashSet<>(binAttributes.permissions());
expectedPermissions.add(PosixFilePermission.OWNER_EXECUTE);
expectedPermissions.add(PosixFilePermission.GROUP_EXECUTE);
expectedPermissions.add(PosixFilePermission.OTHERS_EXECUTE);
assertEquals(expectedPermissions, attributes.permissions());
}
}
}
}
if (Files.exists(original.resolve("config"))) {
Path configDir = env.configFile().resolve(name);
assertTrue("config dir exists", Files.exists(configDir));
assertTrue("config is a dir", Files.isDirectory(configDir));
try (DirectoryStream<Path> stream = Files.newDirectoryStream(configDir)) {
for (Path file : stream) {
assertFalse("not a dir", Files.isDirectory(file));
}
}
}
assertInstallCleaned(env);
}
void assertInstallCleaned(Environment env) throws IOException {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(env.pluginsFile())) {
for (Path file : stream) {
if (file.getFileName().toString().startsWith(".installing")) {
fail("Installation dir still exists, " + file);
}
}
}
}
public void testSomethingWorks() throws Exception {
Environment env = createEnv();
Path pluginDir = createTempDir();
String pluginZip = createPlugin("fake", pluginDir);
installPlugin(pluginZip, env);
assertPlugin("fake", pluginDir, env);
}
public void testPluginsDirMissing() throws Exception {
Environment env = createEnv();
Files.delete(env.pluginsFile());
Path pluginDir = createTempDir();
String pluginZip = createPlugin("fake", pluginDir);
installPlugin(pluginZip, env);
assertPlugin("fake", pluginDir, env);
}
public void testPluginsDirReadOnly() throws Exception {
assumeTrue("posix filesystem", isPosix);
Environment env = createEnv();
try (PosixPermissionsResetter pluginsAttrs = new PosixPermissionsResetter(env.pluginsFile())) {
pluginsAttrs.setPermissions(new HashSet<>());
String pluginZip = createPlugin("fake", createTempDir());
IOException e = expectThrows(IOException.class, () -> {
installPlugin(pluginZip, env);
});
assertTrue(e.getMessage(), e.getMessage().contains("Plugins directory is read only"));
}
assertInstallCleaned(env);
}
public void testBuiltinModule() throws Exception {
Environment env = createEnv();
String pluginZip = createPlugin("lang-groovy", createTempDir());
IOException e = expectThrows(IOException.class, () -> {
installPlugin(pluginZip, env);
});
assertTrue(e.getMessage(), e.getMessage().contains("is a system module"));
assertInstallCleaned(env);
}
public void testJarHell() throws Exception {
Environment env = createEnv();
Path pluginDir = createTempDir();
writeJar(pluginDir.resolve("other.jar"), "FakePlugin");
String pluginZip = createPlugin("fake", pluginDir); // adds plugin.jar with FakePlugin
IllegalStateException e = expectThrows(IllegalStateException.class, () -> {
installPlugin(pluginZip, env);
});
assertTrue(e.getMessage(), e.getMessage().contains("jar hell"));
assertInstallCleaned(env);
}
public void testIsolatedPlugins() throws Exception {
Environment env = createEnv();
// these both share the same FakePlugin class
Path pluginDir1 = createTempDir();
String pluginZip1 = createPlugin("fake1", pluginDir1);
installPlugin(pluginZip1, env);
Path pluginDir2 = createTempDir();
String pluginZip2 = createPlugin("fake2", pluginDir2);
installPlugin(pluginZip2, env);
assertPlugin("fake1", pluginDir1, env);
assertPlugin("fake2", pluginDir2, env);
}
public void testPurgatoryJarHell() throws Exception {
Environment env = createEnv();
Path pluginDir1 = createTempDir();
PluginTestUtil.writeProperties(pluginDir1,
"description", "fake desc",
"name", "fake1",
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin",
"isolated", "false");
writeJar(pluginDir1.resolve("plugin.jar"), "FakePlugin");
String pluginZip1 = writeZip(pluginDir1);
installPlugin(pluginZip1, env);
Path pluginDir2 = createTempDir();
PluginTestUtil.writeProperties(pluginDir2,
"description", "fake desc",
"name", "fake2",
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin",
"isolated", "false");
writeJar(pluginDir2.resolve("plugin.jar"), "FakePlugin");
String pluginZip2 = writeZip(pluginDir2);
IllegalStateException e = expectThrows(IllegalStateException.class, () -> {
installPlugin(pluginZip2, env);
});
assertTrue(e.getMessage(), e.getMessage().contains("jar hell"));
assertInstallCleaned(env);
}
public void testExistingPlugin() throws Exception {
Environment env = createEnv();
String pluginZip = createPlugin("fake", createTempDir());
installPlugin(pluginZip, env);
IOException e = expectThrows(IOException.class, () -> {
installPlugin(pluginZip, env);
});
assertTrue(e.getMessage(), e.getMessage().contains("already exists"));
assertInstallCleaned(env);
}
public void testBin() throws Exception {
Environment env = createEnv();
Path pluginDir = createTempDir();
Path binDir = pluginDir.resolve("bin");
Files.createDirectory(binDir);
Files.createFile(binDir.resolve("somescript"));
String pluginZip = createPlugin("fake", pluginDir);
installPlugin(pluginZip, env);
assertPlugin("fake", pluginDir, env);
}
public void testBinNotDir() throws Exception {
Environment env = createEnv();
Path pluginDir = createTempDir();
Path binDir = pluginDir.resolve("bin");
Files.createFile(binDir);
String pluginZip = createPlugin("fake", pluginDir);
IOException e = expectThrows(IOException.class, () -> {
installPlugin(pluginZip, env);
});
assertTrue(e.getMessage(), e.getMessage().contains("not a directory"));
assertInstallCleaned(env);
}
public void testBinContainsDir() throws Exception {
Environment env = createEnv();
Path pluginDir = createTempDir();
Path dirInBinDir = pluginDir.resolve("bin").resolve("foo");
Files.createDirectories(dirInBinDir);
Files.createFile(dirInBinDir.resolve("somescript"));
String pluginZip = createPlugin("fake", pluginDir);
IOException e = expectThrows(IOException.class, () -> {
installPlugin(pluginZip, env);
});
assertTrue(e.getMessage(), e.getMessage().contains("Directories not allowed in bin dir for plugin"));
assertInstallCleaned(env);
}
public void testBinConflict() throws Exception {
Environment env = createEnv();
Path pluginDir = createTempDir();
Path binDir = pluginDir.resolve("bin");
Files.createDirectory(binDir);
Files.createFile(binDir.resolve("somescript"));
String pluginZip = createPlugin("elasticsearch", pluginDir);
FileAlreadyExistsException e = expectThrows(FileAlreadyExistsException.class, () -> {
installPlugin(pluginZip, env);
});
assertTrue(e.getMessage(), e.getMessage().contains(env.binFile().resolve("elasticsearch").toString()));
assertInstallCleaned(env);
}
public void testBinPermissions() throws Exception {
assumeTrue("posix filesystem", isPosix);
Environment env = createEnv();
Path pluginDir = createTempDir();
Path binDir = pluginDir.resolve("bin");
Files.createDirectory(binDir);
Files.createFile(binDir.resolve("somescript"));
String pluginZip = createPlugin("fake", pluginDir);
try (PosixPermissionsResetter binAttrs = new PosixPermissionsResetter(env.binFile())) {
Set<PosixFilePermission> perms = new HashSet<>(binAttrs.permissions);
// make sure at least one execute perm is missing, so we know we forced it during installation
perms.remove(PosixFilePermission.GROUP_EXECUTE);
binAttrs.setPermissions(perms);
installPlugin(pluginZip, env);
assertPlugin("fake", pluginDir, env);
}
}
public void testConfig() throws Exception {
Environment env = createEnv();
Path pluginDir = createTempDir();
Path configDir = pluginDir.resolve("config");
Files.createDirectory(configDir);
Files.createFile(configDir.resolve("custom.yaml"));
String pluginZip = createPlugin("fake", pluginDir);
installPlugin(pluginZip, env);
assertPlugin("fake", pluginDir, env);
}
public void testExistingConfig() throws Exception {
Environment env = createEnv();
Path envConfigDir = env.configFile().resolve("fake");
Files.createDirectories(envConfigDir);
Files.write(envConfigDir.resolve("custom.yaml"), "existing config".getBytes(StandardCharsets.UTF_8));
Path pluginDir = createTempDir();
Path configDir = pluginDir.resolve("config");
Files.createDirectory(configDir);
Files.write(configDir.resolve("custom.yaml"), "new config".getBytes(StandardCharsets.UTF_8));
Files.createFile(configDir.resolve("other.yaml"));
String pluginZip = createPlugin("fake", pluginDir);
installPlugin(pluginZip, env);
assertPlugin("fake", pluginDir, env);
List<String> configLines = Files.readAllLines(envConfigDir.resolve("custom.yaml"), StandardCharsets.UTF_8);
assertEquals(1, configLines.size());
assertEquals("existing config", configLines.get(0));
assertTrue(Files.exists(envConfigDir.resolve("other.yaml")));
}
public void testConfigNotDir() throws Exception {
Environment env = createEnv();
Path pluginDir = createTempDir();
Path configDir = pluginDir.resolve("config");
Files.createFile(configDir);
String pluginZip = createPlugin("fake", pluginDir);
IOException e = expectThrows(IOException.class, () -> {
installPlugin(pluginZip, env);
});
assertTrue(e.getMessage(), e.getMessage().contains("not a directory"));
assertInstallCleaned(env);
}
public void testConfigContainsDir() throws Exception {
Environment env = createEnv();
Path pluginDir = createTempDir();
Path dirInConfigDir = pluginDir.resolve("config").resolve("foo");
Files.createDirectories(dirInConfigDir);
Files.createFile(dirInConfigDir.resolve("myconfig.yml"));
String pluginZip = createPlugin("fake", pluginDir);
IOException e = expectThrows(IOException.class, () -> {
installPlugin(pluginZip, env);
});
assertTrue(e.getMessage(), e.getMessage().contains("Directories not allowed in config dir for plugin"));
assertInstallCleaned(env);
}
public void testConfigConflict() throws Exception {
Environment env = createEnv();
Path pluginDir = createTempDir();
Path configDir = pluginDir.resolve("config");
Files.createDirectory(configDir);
Files.createFile(configDir.resolve("myconfig.yml"));
String pluginZip = createPlugin("elasticsearch.yml", pluginDir);
FileAlreadyExistsException e = expectThrows(FileAlreadyExistsException.class, () -> {
installPlugin(pluginZip, env);
});
assertTrue(e.getMessage(), e.getMessage().contains(env.configFile().resolve("elasticsearch.yml").toString()));
assertInstallCleaned(env);
}
// TODO: test batch flag?
// TODO: test checksum (need maven/official below)
// TODO: test maven, official, and staging install...need tests with fixtures...
}

View File

@ -0,0 +1,90 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import org.apache.lucene.util.LuceneTestCase;
import org.elasticsearch.common.cli.CliTool;
import org.elasticsearch.common.cli.CliToolTestCase;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
@LuceneTestCase.SuppressFileSystems("*")
public class ListPluginsCommandTests extends ESTestCase {
Environment createEnv() throws IOException {
Path home = createTempDir();
Files.createDirectories(home.resolve("plugins"));
Settings settings = Settings.builder()
.put("path.home", home)
.build();
return new Environment(settings);
}
static CliToolTestCase.CaptureOutputTerminal listPlugins(Environment env) throws Exception {
CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal(Terminal.Verbosity.NORMAL);
CliTool.ExitStatus status = new ListPluginsCommand(terminal).execute(env.settings(), env);
assertEquals(CliTool.ExitStatus.OK, status);
return terminal;
}
public void testPluginsDirMissing() throws Exception {
Environment env = createEnv();
Files.delete(env.pluginsFile());
IOException e = expectThrows(IOException.class, () -> {
listPlugins(env);
});
assertTrue(e.getMessage(), e.getMessage().contains("Plugins directory missing"));
}
public void testNoPlugins() throws Exception {
CliToolTestCase.CaptureOutputTerminal terminal = listPlugins(createEnv());
List<String> lines = terminal.getTerminalOutput();
assertEquals(0, lines.size());
}
public void testOnePlugin() throws Exception {
Environment env = createEnv();
Files.createDirectory(env.pluginsFile().resolve("fake"));
CliToolTestCase.CaptureOutputTerminal terminal = listPlugins(env);
List<String> lines = terminal.getTerminalOutput();
assertEquals(1, lines.size());
assertTrue(lines.get(0).contains("fake"));
}
public void testTwoPlugins() throws Exception {
Environment env = createEnv();
Files.createDirectory(env.pluginsFile().resolve("fake1"));
Files.createDirectory(env.pluginsFile().resolve("fake2"));
CliToolTestCase.CaptureOutputTerminal terminal = listPlugins(env);
List<String> lines = terminal.getTerminalOutput();
assertEquals(2, lines.size());
Collections.sort(lines);
assertTrue(lines.get(0).contains("fake1"));
assertTrue(lines.get(1).contains("fake2"));
}
}

View File

@ -1,377 +0,0 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import org.apache.lucene.util.LuceneTestCase;
import org.elasticsearch.Version;
import org.elasticsearch.common.cli.CliToolTestCase.CaptureOutputTerminal;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.junit.Before;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static java.nio.file.attribute.PosixFilePermission.GROUP_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.OTHERS_EXECUTE;
import static java.nio.file.attribute.PosixFilePermission.OWNER_EXECUTE;
import static org.elasticsearch.common.settings.Settings.settingsBuilder;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertDirectoryExists;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFileExists;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFileNotExists;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
// there are some lucene file systems that seem to cause problems (deleted files, dirs instead of files)
@LuceneTestCase.SuppressFileSystems("*")
public class PluginManagerPermissionTests extends ESTestCase {
private String pluginName = "my-plugin";
private CaptureOutputTerminal terminal = new CaptureOutputTerminal();
private Environment environment;
private boolean supportsPermissions;
@Before
public void setup() {
Path tempDir = createTempDir();
Settings.Builder settingsBuilder = settingsBuilder().put(Environment.PATH_HOME_SETTING.getKey(), tempDir);
if (randomBoolean()) {
settingsBuilder.put(Environment.PATH_PLUGINS_SETTING.getKey(), createTempDir());
}
if (randomBoolean()) {
settingsBuilder.put(Environment.PATH_CONF_SETTING.getKey(), createTempDir());
}
environment = new Environment(settingsBuilder.build());
supportsPermissions = tempDir.getFileSystem().supportedFileAttributeViews().contains("posix");
}
public void testThatUnaccessibleBinDirectoryAbortsPluginInstallation() throws Exception {
assumeTrue("File system does not support permissions, skipping", supportsPermissions);
URL pluginUrl = createPlugin(true, randomBoolean());
Path binPath = environment.binFile().resolve(pluginName);
Files.createDirectories(binPath);
try {
Files.setPosixFilePermissions(binPath, PosixFilePermissions.fromString("---------"));
PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10));
pluginManager.downloadAndExtract(pluginName, terminal, true);
fail("Expected IOException but did not happen");
} catch (IOException e) {
assertFileNotExists(environment.pluginsFile().resolve(pluginName));
assertFileNotExists(environment.configFile().resolve(pluginName));
// exists, because of our weird permissions above
assertDirectoryExists(environment.binFile().resolve(pluginName));
assertThat(terminal.getTerminalOutput(), hasItem(containsString("Error copying bin directory ")));
} finally {
Files.setPosixFilePermissions(binPath, PosixFilePermissions.fromString("rwxrwxrwx"));
}
}
public void testThatUnaccessiblePluginConfigDirectoryAbortsPluginInstallation() throws Exception {
assumeTrue("File system does not support permissions, skipping", supportsPermissions);
URL pluginUrl = createPlugin(randomBoolean(), true);
Path path = environment.configFile().resolve(pluginName);
Files.createDirectories(path);
Files.createFile(path.resolve("my-custom-config.yaml"));
Path binPath = environment.binFile().resolve(pluginName);
Files.createDirectories(binPath);
try {
Files.setPosixFilePermissions(path.resolve("my-custom-config.yaml"), PosixFilePermissions.fromString("---------"));
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("---------"));
PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10));
pluginManager.downloadAndExtract(pluginName, terminal, true);
fail("Expected IOException but did not happen, terminal output was " + terminal.getTerminalOutput());
} catch (IOException e) {
assertFileNotExists(environment.pluginsFile().resolve(pluginName));
assertFileNotExists(environment.binFile().resolve(pluginName));
// exists, because of our weird permissions above
assertDirectoryExists(environment.configFile().resolve(pluginName));
assertThat(terminal.getTerminalOutput(), hasItem(containsString("Error copying config directory ")));
} finally {
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rwxrwxrwx"));
Files.setPosixFilePermissions(path.resolve("my-custom-config.yaml"), PosixFilePermissions.fromString("rwxrwxrwx"));
}
}
// config/bin are not writable, but the plugin does not need to put anything into it
public void testThatPluginWithoutBinAndConfigWorksEvenIfPermissionsAreWrong() throws Exception {
assumeTrue("File system does not support permissions, skipping", supportsPermissions);
URL pluginUrl = createPlugin(false, false);
Path path = environment.configFile().resolve(pluginName);
Files.createDirectories(path);
Files.createFile(path.resolve("my-custom-config.yaml"));
Path binPath = environment.binFile().resolve(pluginName);
Files.createDirectories(binPath);
try {
Files.setPosixFilePermissions(path.resolve("my-custom-config.yaml"), PosixFilePermissions.fromString("---------"));
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("---------"));
Files.setPosixFilePermissions(binPath, PosixFilePermissions.fromString("---------"));
PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10));
pluginManager.downloadAndExtract(pluginName, terminal, true);
} finally {
Files.setPosixFilePermissions(binPath, PosixFilePermissions.fromString("rwxrwxrwx"));
Files.setPosixFilePermissions(path, PosixFilePermissions.fromString("rwxrwxrwx"));
Files.setPosixFilePermissions(path.resolve("my-custom-config.yaml"), PosixFilePermissions.fromString("rwxrwxrwx"));
}
}
// plugins directory no accessible, should leave no other left over directories
public void testThatNonWritablePluginsDirectoryLeavesNoLeftOver() throws Exception {
assumeTrue("File system does not support permissions, skipping", supportsPermissions);
URL pluginUrl = createPlugin(true, true);
Files.createDirectories(environment.pluginsFile());
try {
Files.setPosixFilePermissions(environment.pluginsFile(), PosixFilePermissions.fromString("---------"));
PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10));
try {
pluginManager.downloadAndExtract(pluginName, terminal, true);
fail("Expected IOException due to read-only plugins/ directory");
} catch (IOException e) {
assertFileNotExists(environment.binFile().resolve(pluginName));
assertFileNotExists(environment.configFile().resolve(pluginName));
Files.setPosixFilePermissions(environment.pluginsFile(), PosixFilePermissions.fromString("rwxrwxrwx"));
assertDirectoryExists(environment.pluginsFile());
assertFileNotExists(environment.pluginsFile().resolve(pluginName));
}
} finally {
Files.setPosixFilePermissions(environment.pluginsFile(), PosixFilePermissions.fromString("rwxrwxrwx"));
}
}
public void testThatUnwriteableBackupFilesInConfigurationDirectoryAreReplaced() throws Exception {
assumeTrue("File system does not support permissions, skipping", supportsPermissions);
boolean pluginContainsExecutables = randomBoolean();
URL pluginUrl = createPlugin(pluginContainsExecutables, true);
Files.createDirectories(environment.configFile().resolve(pluginName));
Path configFile = environment.configFile().resolve(pluginName).resolve("my-custom-config.yaml");
Files.createFile(configFile);
Path backupConfigFile = environment.configFile().resolve(pluginName).resolve("my-custom-config.yaml.new");
Files.createFile(backupConfigFile);
Files.write(backupConfigFile, "foo".getBytes(Charset.forName("UTF-8")));
PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10));
try {
Files.setPosixFilePermissions(backupConfigFile, PosixFilePermissions.fromString("---------"));
pluginManager.downloadAndExtract(pluginName, terminal, true);
if (pluginContainsExecutables) {
assertDirectoryExists(environment.binFile().resolve(pluginName));
}
assertDirectoryExists(environment.pluginsFile().resolve(pluginName));
assertDirectoryExists(environment.configFile().resolve(pluginName));
assertFileExists(backupConfigFile);
Files.setPosixFilePermissions(backupConfigFile, PosixFilePermissions.fromString("rw-rw-rw-"));
String content = new String(Files.readAllBytes(backupConfigFile), Charset.forName("UTF-8"));
assertThat(content, is(not("foo")));
} finally {
Files.setPosixFilePermissions(backupConfigFile, PosixFilePermissions.fromString("rw-rw-rw-"));
}
}
public void testThatConfigDirectoryBeingAFileAbortsInstallationAndDoesNotAccidentallyDeleteThisFile() throws Exception {
assumeTrue("File system does not support permissions, skipping", supportsPermissions);
Files.createDirectories(environment.configFile());
Files.createFile(environment.configFile().resolve(pluginName));
URL pluginUrl = createPlugin(randomBoolean(), true);
PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10));
try {
pluginManager.downloadAndExtract(pluginName, terminal, true);
fail("Expected plugin installation to fail, but didnt");
} catch (IOException e) {
assertFileExists(environment.configFile().resolve(pluginName));
assertFileNotExists(environment.binFile().resolve(pluginName));
assertFileNotExists(environment.pluginsFile().resolve(pluginName));
}
}
public void testThatBinDirectoryBeingAFileAbortsInstallationAndDoesNotAccidentallyDeleteThisFile() throws Exception {
assumeTrue("File system does not support permissions, skipping", supportsPermissions);
Files.createDirectories(environment.binFile());
Files.createFile(environment.binFile().resolve(pluginName));
URL pluginUrl = createPlugin(true, randomBoolean());
PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10));
try {
pluginManager.downloadAndExtract(pluginName, terminal, true);
fail("Expected plugin installation to fail, but didnt");
} catch (IOException e) {
assertFileExists(environment.binFile().resolve(pluginName));
assertFileNotExists(environment.configFile().resolve(pluginName));
assertFileNotExists(environment.pluginsFile().resolve(pluginName));
}
}
public void testConfigDirectoryOwnerGroupAndPermissions() throws IOException {
assumeTrue("File system does not support permissions, skipping", supportsPermissions);
URL pluginUrl = createPlugin(false, true);
PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10));
pluginManager.downloadAndExtract(pluginName, terminal, true);
PosixFileAttributes parentFileAttributes = Files.getFileAttributeView(environment.configFile(), PosixFileAttributeView.class).readAttributes();
Path configPath = environment.configFile().resolve(pluginName);
PosixFileAttributes pluginConfigDirAttributes = Files.getFileAttributeView(configPath, PosixFileAttributeView.class).readAttributes();
assertThat(pluginConfigDirAttributes.owner(), equalTo(parentFileAttributes.owner()));
assertThat(pluginConfigDirAttributes.group(), equalTo(parentFileAttributes.group()));
assertThat(pluginConfigDirAttributes.permissions(), equalTo(parentFileAttributes.permissions()));
Path configFile = configPath.resolve("my-custom-config.yaml");
PosixFileAttributes pluginConfigFileAttributes = Files.getFileAttributeView(configFile, PosixFileAttributeView.class).readAttributes();
assertThat(pluginConfigFileAttributes.owner(), equalTo(parentFileAttributes.owner()));
assertThat(pluginConfigFileAttributes.group(), equalTo(parentFileAttributes.group()));
Set<PosixFilePermission> expectedFilePermissions = new HashSet<>();
for (PosixFilePermission parentPermission : parentFileAttributes.permissions()) {
switch(parentPermission) {
case OWNER_EXECUTE:
case GROUP_EXECUTE:
case OTHERS_EXECUTE:
break;
default:
expectedFilePermissions.add(parentPermission);
}
}
assertThat(pluginConfigFileAttributes.permissions(), equalTo(expectedFilePermissions));
}
public void testBinDirectoryOwnerGroupAndPermissions() throws IOException {
assumeTrue("File system does not support permissions, skipping", supportsPermissions);
URL pluginUrl = createPlugin(true, false);
PluginManager pluginManager = new PluginManager(environment, pluginUrl, PluginManager.OutputMode.VERBOSE, TimeValue.timeValueSeconds(10));
pluginManager.downloadAndExtract(pluginName, terminal, true);
PosixFileAttributes parentFileAttributes = Files.getFileAttributeView(environment.binFile(), PosixFileAttributeView.class).readAttributes();
Path binPath = environment.binFile().resolve(pluginName);
PosixFileAttributes pluginBinDirAttributes = Files.getFileAttributeView(binPath, PosixFileAttributeView.class).readAttributes();
assertThat(pluginBinDirAttributes.owner(), equalTo(parentFileAttributes.owner()));
assertThat(pluginBinDirAttributes.group(), equalTo(parentFileAttributes.group()));
assertThat(pluginBinDirAttributes.permissions(), equalTo(parentFileAttributes.permissions()));
Path executableFile = binPath.resolve("my-binary");
PosixFileAttributes pluginExecutableFileAttributes = Files.getFileAttributeView(executableFile, PosixFileAttributeView.class).readAttributes();
assertThat(pluginExecutableFileAttributes.owner(), equalTo(parentFileAttributes.owner()));
assertThat(pluginExecutableFileAttributes.group(), equalTo(parentFileAttributes.group()));
Set<PosixFilePermission> expectedFilePermissions = new HashSet<>();
expectedFilePermissions.add(OWNER_EXECUTE);
expectedFilePermissions.add(GROUP_EXECUTE);
expectedFilePermissions.add(OTHERS_EXECUTE);
for (PosixFilePermission parentPermission : parentFileAttributes.permissions()) {
switch(parentPermission) {
case OWNER_EXECUTE:
case GROUP_EXECUTE:
case OTHERS_EXECUTE:
break;
default:
expectedFilePermissions.add(parentPermission);
}
}
assertThat(pluginExecutableFileAttributes.permissions(), equalTo(expectedFilePermissions));
}
private URL createPlugin(boolean withBinDir, boolean withConfigDir) throws IOException {
final Path structure = createTempDir().resolve("fake-plugin");
PluginTestUtil.writeProperties(structure, "description", "fake desc",
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"jvm", "true",
"java.version", "1.7",
"name", pluginName,
"classname", pluginName);
if (withBinDir) {
// create bin dir
Path binDir = structure.resolve("bin");
Files.createDirectory(binDir);
Files.setPosixFilePermissions(binDir, PosixFilePermissions.fromString("rwxr-xr-x"));
// create executable
Path executable = binDir.resolve("my-binary");
Files.createFile(executable);
Files.setPosixFilePermissions(executable, PosixFilePermissions.fromString("rw-r--r--"));
}
if (withConfigDir) {
// create bin dir
Path configDir = structure.resolve("config");
Files.createDirectory(configDir);
Files.setPosixFilePermissions(configDir, PosixFilePermissions.fromString("rwxr-xr-x"));
// create config file
Path configFile = configDir.resolve("my-custom-config.yaml");
Files.createFile(configFile);
Files.write(configFile, "my custom config content".getBytes(Charset.forName("UTF-8")));
Files.setPosixFilePermissions(configFile, PosixFilePermissions.fromString("rw-r--r--"));
}
Path zip = createTempDir().resolve(structure.getFileName() + ".zip");
try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zip))) {
Files.walkFileTree(structure, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
stream.putNextEntry(new ZipEntry(structure.relativize(file).toString()));
Files.copy(file, stream);
return FileVisitResult.CONTINUE;
}
});
}
return zip.toUri().toURL();
}
}

View File

@ -1,725 +0,0 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import org.apache.http.impl.client.HttpClients;
import org.apache.lucene.util.LuceneTestCase;
import org.elasticsearch.Version;
import org.elasticsearch.common.Base64;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.cli.CliTool;
import org.elasticsearch.common.cli.CliTool.ExitStatus;
import org.elasticsearch.common.cli.CliToolTestCase.CaptureOutputTerminal;
import org.elasticsearch.common.hash.MessageDigests;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.node.internal.InternalSettingsPreparer;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.test.ESIntegTestCase.ClusterScope;
import org.elasticsearch.test.junit.annotations.Network;
import org.elasticsearch.test.rest.client.http.HttpRequestBuilder;
import org.elasticsearch.test.rest.client.http.HttpResponse;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
import org.jboss.netty.handler.codec.http.HttpResponseEncoder;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.ssl.SslContext;
import org.jboss.netty.handler.ssl.SslHandler;
import org.jboss.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.jboss.netty.handler.ssl.util.SelfSignedCertificate;
import org.junit.After;
import org.junit.Before;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import java.io.BufferedWriter;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.PosixFileAttributeView;
import java.nio.file.attribute.PosixFileAttributes;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static org.elasticsearch.common.cli.CliTool.ExitStatus.USAGE;
import static org.elasticsearch.common.cli.CliToolTestCase.args;
import static org.elasticsearch.common.io.FileTestUtils.assertFileContent;
import static org.elasticsearch.common.settings.Settings.settingsBuilder;
import static org.elasticsearch.test.ESIntegTestCase.Scope;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertDirectoryExists;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFileExists;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertFileNotExists;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.jboss.netty.handler.codec.http.HttpVersion.HTTP_1_1;
@ClusterScope(scope = Scope.TEST, numDataNodes = 0, transportClientRatio = 0.0)
@LuceneTestCase.SuppressFileSystems("*") // TODO: clean up this test to allow extra files
// TODO: jimfs is really broken here (throws wrong exception from detection method).
// if its in your classpath, then do not use plugins!!!!!!
@SuppressForbidden(reason = "modifies system properties intentionally")
public class PluginManagerTests extends ESIntegTestCase {
private Environment environment;
private CaptureOutputTerminal terminal = new CaptureOutputTerminal();
@Before
public void setup() throws Exception {
environment = buildInitialSettings();
System.setProperty("es.default.path.home", Environment.PATH_HOME_SETTING.get(environment.settings()));
Path binDir = environment.binFile();
if (!Files.exists(binDir)) {
Files.createDirectories(binDir);
}
Path configDir = environment.configFile();
if (!Files.exists(configDir)) {
Files.createDirectories(configDir);
}
}
@After
public void clearPathHome() {
System.clearProperty("es.default.path.home");
}
private void writeSha1(Path file, boolean corrupt) throws IOException {
String sha1Hex = MessageDigests.toHexString(MessageDigests.sha1().digest(Files.readAllBytes(file)));
try (BufferedWriter out = Files.newBufferedWriter(file.resolveSibling(file.getFileName() + ".sha1"), StandardCharsets.UTF_8)) {
out.write(sha1Hex);
if (corrupt) {
out.write("bad");
}
}
}
private void writeMd5(Path file, boolean corrupt) throws IOException {
String md5Hex = MessageDigests.toHexString(MessageDigests.md5().digest(Files.readAllBytes(file)));
try (BufferedWriter out = Files.newBufferedWriter(file.resolveSibling(file.getFileName() + ".md5"), StandardCharsets.UTF_8)) {
out.write(md5Hex);
if (corrupt) {
out.write("bad");
}
}
}
/** creates a plugin .zip and returns the url for testing */
private String createPlugin(final Path structure, String... properties) throws IOException {
PluginTestUtil.writeProperties(structure, properties);
Path zip = createTempDir().resolve(structure.getFileName() + ".zip");
try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zip))) {
Files.walkFileTree(structure, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
stream.putNextEntry(new ZipEntry(structure.relativize(file).toString()));
Files.copy(file, stream);
return FileVisitResult.CONTINUE;
}
});
}
if (randomBoolean()) {
writeSha1(zip, false);
} else if (randomBoolean()) {
writeMd5(zip, false);
}
return zip.toUri().toURL().toString();
}
/** creates a plugin .zip and bad checksum file and returns the url for testing */
private String createPluginWithBadChecksum(final Path structure, String... properties) throws IOException {
PluginTestUtil.writeProperties(structure, properties);
Path zip = createTempDir().resolve(structure.getFileName() + ".zip");
try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zip))) {
Files.walkFileTree(structure, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
stream.putNextEntry(new ZipEntry(structure.relativize(file).toString()));
Files.copy(file, stream);
return FileVisitResult.CONTINUE;
}
});
}
if (randomBoolean()) {
writeSha1(zip, true);
} else {
writeMd5(zip, true);
}
return zip.toUri().toURL().toString();
}
public void testThatPluginNameMustBeSupplied() throws IOException {
Path pluginDir = createTempDir().resolve("fake-plugin");
String pluginUrl = createPlugin(pluginDir,
"description", "fake desc",
"name", "fake-plugin",
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin");
assertStatus("install", USAGE);
}
public void testLocalPluginInstallWithBinAndConfig() throws Exception {
String pluginName = "fake-plugin";
Path pluginDir = createTempDir().resolve(pluginName);
// create bin/tool and config/file
Files.createDirectories(pluginDir.resolve("bin"));
Files.createFile(pluginDir.resolve("bin").resolve("tool"));
Files.createDirectories(pluginDir.resolve("config"));
Files.createFile(pluginDir.resolve("config").resolve("file"));
String pluginUrl = createPlugin(pluginDir,
"description", "fake desc",
"name", pluginName,
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin");
Path binDir = environment.binFile();
Path pluginBinDir = binDir.resolve(pluginName);
Path pluginConfigDir = environment.configFile().resolve(pluginName);
assertStatusOk("install " + pluginUrl + " --verbose");
terminal.getTerminalOutput().clear();
assertStatusOk("list");
assertThat(terminal.getTerminalOutput(), hasItem(containsString(pluginName)));
assertDirectoryExists(pluginBinDir);
assertDirectoryExists(pluginConfigDir);
Path toolFile = pluginBinDir.resolve("tool");
assertFileExists(toolFile);
// check that the file is marked executable, without actually checking that we can execute it.
PosixFileAttributeView view = Files.getFileAttributeView(toolFile, PosixFileAttributeView.class);
// the view might be null, on e.g. windows, there is nothing to check there!
if (view != null) {
PosixFileAttributes attributes = view.readAttributes();
assertThat(attributes.permissions(), hasItem(PosixFilePermission.OWNER_EXECUTE));
assertThat(attributes.permissions(), hasItem(PosixFilePermission.OWNER_READ));
}
}
/**
* Test for #7890
*/
public void testLocalPluginInstallWithBinAndConfigInAlreadyExistingConfigDir_7890() throws Exception {
String pluginName = "fake-plugin";
Path pluginDir = createTempDir().resolve(pluginName);
// create config/test.txt with contents 'version1'
Files.createDirectories(pluginDir.resolve("config"));
Files.write(pluginDir.resolve("config").resolve("test.txt"), "version1".getBytes(StandardCharsets.UTF_8));
String pluginUrl = createPlugin(pluginDir,
"description", "fake desc",
"name", pluginName,
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin");
Path pluginConfigDir = environment.configFile().resolve(pluginName);
assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginUrl));
/*
First time, our plugin contains:
- config/test.txt (version1)
*/
assertFileContent(pluginConfigDir, "test.txt", "version1");
// We now remove the plugin
assertStatusOk("remove " + pluginName);
// We should still have test.txt
assertFileContent(pluginConfigDir, "test.txt", "version1");
// Installing a new plugin version
/*
Second time, our plugin contains:
- config/test.txt (version2)
- config/dir/testdir.txt (version1)
- config/dir/subdir/testsubdir.txt (version1)
*/
Files.write(pluginDir.resolve("config").resolve("test.txt"), "version2".getBytes(StandardCharsets.UTF_8));
Files.createDirectories(pluginDir.resolve("config").resolve("dir").resolve("subdir"));
Files.write(pluginDir.resolve("config").resolve("dir").resolve("testdir.txt"), "version1".getBytes(StandardCharsets.UTF_8));
Files.write(pluginDir.resolve("config").resolve("dir").resolve("subdir").resolve("testsubdir.txt"), "version1".getBytes(StandardCharsets.UTF_8));
pluginUrl = createPlugin(pluginDir,
"description", "fake desc",
"name", pluginName,
"version", "2.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin");
assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginUrl));
assertFileContent(pluginConfigDir, "test.txt", "version1");
assertFileContent(pluginConfigDir, "test.txt.new", "version2");
assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1");
assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1");
// Removing
assertStatusOk("remove " + pluginName);
assertFileContent(pluginConfigDir, "test.txt", "version1");
assertFileContent(pluginConfigDir, "test.txt.new", "version2");
assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1");
assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1");
// Installing a new plugin version
/*
Third time, our plugin contains:
- config/test.txt (version3)
- config/test2.txt (version1)
- config/dir/testdir.txt (version2)
- config/dir/testdir2.txt (version1)
- config/dir/subdir/testsubdir.txt (version2)
*/
Files.write(pluginDir.resolve("config").resolve("test.txt"), "version3".getBytes(StandardCharsets.UTF_8));
Files.write(pluginDir.resolve("config").resolve("test2.txt"), "version1".getBytes(StandardCharsets.UTF_8));
Files.write(pluginDir.resolve("config").resolve("dir").resolve("testdir.txt"), "version2".getBytes(StandardCharsets.UTF_8));
Files.write(pluginDir.resolve("config").resolve("dir").resolve("testdir2.txt"), "version1".getBytes(StandardCharsets.UTF_8));
Files.write(pluginDir.resolve("config").resolve("dir").resolve("subdir").resolve("testsubdir.txt"), "version2".getBytes(StandardCharsets.UTF_8));
pluginUrl = createPlugin(pluginDir,
"description", "fake desc",
"name", pluginName,
"version", "3.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"jvm", "true",
"classname", "FakePlugin");
assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginUrl));
assertFileContent(pluginConfigDir, "test.txt", "version1");
assertFileContent(pluginConfigDir, "test2.txt", "version1");
assertFileContent(pluginConfigDir, "test.txt.new", "version3");
assertFileContent(pluginConfigDir, "dir/testdir.txt", "version1");
assertFileContent(pluginConfigDir, "dir/testdir.txt.new", "version2");
assertFileContent(pluginConfigDir, "dir/testdir2.txt", "version1");
assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt", "version1");
assertFileContent(pluginConfigDir, "dir/subdir/testsubdir.txt.new", "version2");
}
// For #7152
public void testLocalPluginInstallWithBinOnly_7152() throws Exception {
String pluginName = "fake-plugin";
Path pluginDir = createTempDir().resolve(pluginName);
// create bin/tool
Files.createDirectories(pluginDir.resolve("bin"));
Files.createFile(pluginDir.resolve("bin").resolve("tool"));;
String pluginUrl = createPlugin(pluginDir,
"description", "fake desc",
"name", "fake-plugin",
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin");
Path binDir = environment.binFile();
Path pluginBinDir = binDir.resolve(pluginName);
assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginUrl));
assertThatPluginIsListed(pluginName);
assertDirectoryExists(pluginBinDir);
}
public void testListInstalledEmpty() throws IOException {
assertStatusOk("list");
assertThat(terminal.getTerminalOutput(), hasItem(containsString("No plugin detected")));
}
public void testListInstalledEmptyWithExistingPluginDirectory() throws IOException {
Files.createDirectory(environment.pluginsFile());
assertStatusOk("list");
assertThat(terminal.getTerminalOutput(), hasItem(containsString("No plugin detected")));
}
public void testInstallPluginVerbose() throws IOException {
String pluginName = "fake-plugin";
Path pluginDir = createTempDir().resolve(pluginName);
String pluginUrl = createPlugin(pluginDir,
"description", "fake desc",
"name", pluginName,
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin");
System.err.println("install " + pluginUrl + " --verbose");
ExitStatus status = new PluginManagerCliParser(terminal).execute(args("install " + pluginUrl + " --verbose"));
assertThat("Terminal output was: " + terminal.getTerminalOutput(), status, is(ExitStatus.OK));
assertThat(terminal.getTerminalOutput(), hasItem(containsString("Name: fake-plugin")));
assertThat(terminal.getTerminalOutput(), hasItem(containsString("Description: fake desc")));
assertThat(terminal.getTerminalOutput(), hasItem(containsString("Version: 1.0")));
assertThatPluginIsListed(pluginName);
}
public void testInstallPlugin() throws IOException {
String pluginName = "fake-plugin";
Path pluginDir = createTempDir().resolve(pluginName);
String pluginUrl = createPlugin(pluginDir,
"description", "fake desc",
"name", pluginName,
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin");
ExitStatus status = new PluginManagerCliParser(terminal).execute(args("install " + pluginUrl));
assertThat("Terminal output was: " + terminal.getTerminalOutput(), status, is(ExitStatus.OK));
assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("Name: fake-plugin"))));
assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("Description:"))));
assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("Site:"))));
assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("Version:"))));
assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("JVM:"))));
assertThatPluginIsListed(pluginName);
}
/**
* @deprecated support for this is not going to stick around, seriously.
*/
@Deprecated
public void testAlreadyInstalledNotIsolated() throws Exception {
String pluginName = "fake-plugin";
Path pluginDir = createTempDir().resolve(pluginName);
Files.createDirectories(pluginDir);
// create a jar file in the plugin
Path pluginJar = pluginDir.resolve("fake-plugin.jar");
try (ZipOutputStream out = new JarOutputStream(Files.newOutputStream(pluginJar, StandardOpenOption.CREATE))) {
out.putNextEntry(new ZipEntry("foo.class"));
out.closeEntry();
}
String pluginUrl = createPlugin(pluginDir,
"description", "fake desc",
"name", pluginName,
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"isolated", "false",
"classname", "FakePlugin");
// install
ExitStatus status = new PluginManagerCliParser(terminal).execute(args("install " + pluginUrl));
assertEquals("unexpected exit status: output: " + terminal.getTerminalOutput(), ExitStatus.OK, status);
// install again
status = new PluginManagerCliParser(terminal).execute(args("install " + pluginUrl));
List<String> output = terminal.getTerminalOutput();
assertEquals("unexpected exit status: output: " + output, ExitStatus.IO_ERROR, status);
boolean foundExpectedMessage = false;
for (String line : output) {
foundExpectedMessage |= line.contains("already exists");
}
assertTrue(foundExpectedMessage);
}
public void testInstallPluginWithBadChecksum() throws IOException {
String pluginName = "fake-plugin";
Path pluginDir = createTempDir().resolve(pluginName);
String pluginUrl = createPluginWithBadChecksum(pluginDir,
"description", "fake desc",
"name", pluginName,
"version", "1.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin");
assertStatus(String.format(Locale.ROOT, "install %s --verbose", pluginUrl),
ExitStatus.IO_ERROR);
assertThatPluginIsNotListed(pluginName);
assertFileNotExists(environment.pluginsFile().resolve(pluginName));
}
private void singlePluginInstallAndRemove(String pluginDescriptor, String pluginName, String pluginCoordinates) throws IOException {
logger.info("--> trying to download and install [{}]", pluginDescriptor);
if (pluginCoordinates == null) {
assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginDescriptor));
} else {
assertStatusOk(String.format(Locale.ROOT, "install %s --verbose", pluginCoordinates));
}
assertThatPluginIsListed(pluginName);
terminal.getTerminalOutput().clear();
assertStatusOk("remove " + pluginDescriptor);
assertThat(terminal.getTerminalOutput(), hasItem(containsString("Removing " + pluginDescriptor)));
// not listed anymore
terminal.getTerminalOutput().clear();
assertStatusOk("list");
assertThat(terminal.getTerminalOutput(), not(hasItem(containsString(pluginName))));
}
/**
* We are ignoring by default these tests as they require to have an internet access
* To activate the test, use -Dtests.network=true
* We test regular form: username/reponame/version
* It should find it in download.elasticsearch.org service
*/
@Network
@AwaitsFix(bugUrl = "fails with jar hell failures - http://build-us-00.elastic.co/job/es_core_master_oracle_6/519/testReport/")
public void testInstallPluginWithElasticsearchDownloadService() throws IOException {
assumeTrue("download.elastic.co is accessible", isDownloadServiceWorking("download.elastic.co", 80, "/elasticsearch/ci-test.txt"));
singlePluginInstallAndRemove("elasticsearch/elasticsearch-transport-thrift/2.4.0", "elasticsearch-transport-thrift", null);
}
/**
* We are ignoring by default these tests as they require to have an internet access
* To activate the test, use -Dtests.network=true
* We test regular form: groupId/artifactId/version
* It should find it in maven central service
*/
@Network
@AwaitsFix(bugUrl = "fails with jar hell failures - http://build-us-00.elastic.co/job/es_core_master_oracle_6/519/testReport/")
public void testInstallPluginWithMavenCentral() throws IOException {
assumeTrue("search.maven.org is accessible", isDownloadServiceWorking("search.maven.org", 80, "/"));
assumeTrue("repo1.maven.org is accessible", isDownloadServiceWorking("repo1.maven.org", 443, "/maven2/org/elasticsearch/elasticsearch-transport-thrift/2.4.0/elasticsearch-transport-thrift-2.4.0.pom"));
singlePluginInstallAndRemove("org.elasticsearch/elasticsearch-transport-thrift/2.4.0", "elasticsearch-transport-thrift", null);
}
/**
* We are ignoring by default these tests as they require to have an internet access
* To activate the test, use -Dtests.network=true
* We test site plugins from github: userName/repoName
* It should find it on github
*/
@Network @AwaitsFix(bugUrl = "needs to be adapted to 2.0")
public void testInstallPluginWithGithub() throws IOException {
assumeTrue("github.com is accessible", isDownloadServiceWorking("github.com", 443, "/"));
singlePluginInstallAndRemove("elasticsearch/kibana", "kibana", null);
}
private boolean isDownloadServiceWorking(String host, int port, String resource) {
try {
String protocol = port == 443 ? "https" : "http";
HttpResponse response = new HttpRequestBuilder(HttpClients.createDefault()).protocol(protocol).host(host).port(port).path(resource).execute();
if (response.getStatusCode() != 200) {
logger.warn("[{}{}] download service is not working. Disabling current test.", host, resource);
return false;
}
return true;
} catch (Throwable t) {
logger.warn("[{}{}] download service is not working. Disabling current test.", host, resource);
}
return false;
}
public void testRemovePlugin() throws Exception {
String pluginName = "plugintest";
Path pluginDir = createTempDir().resolve(pluginName);
String pluginUrl = createPlugin(pluginDir,
"description", "fake desc",
"name", pluginName,
"version", "1.0.0",
"elasticsearch.version", Version.CURRENT.toString(),
"java.version", System.getProperty("java.specification.version"),
"classname", "FakePlugin");
// We want to remove plugin with plugin short name
singlePluginInstallAndRemove("plugintest", "plugintest", pluginUrl);
// We want to remove plugin with groupid/artifactid/version form
singlePluginInstallAndRemove("groupid/plugintest/1.0.0", "plugintest", pluginUrl);
// We want to remove plugin with groupid/artifactid form
singlePluginInstallAndRemove("groupid/plugintest", "plugintest", pluginUrl);
}
public void testRemovePlugin_NullName_ThrowsException() throws IOException {
assertStatus("remove ", USAGE);
}
public void testRemovePluginWithURLForm() throws Exception {
assertStatus("remove file://whatever", USAGE);
assertThat(terminal.getTerminalOutput(), hasItem(containsString("Illegal plugin name")));
}
public void testForbiddenPluginNames() throws IOException {
assertStatus("remove elasticsearch", USAGE);
assertStatus("remove elasticsearch.bat", USAGE);
assertStatus("remove elasticsearch.in.sh", USAGE);
assertStatus("remove plugin", USAGE);
assertStatus("remove plugin.bat", USAGE);
assertStatus("remove service.bat", USAGE);
assertStatus("remove ELASTICSEARCH", USAGE);
assertStatus("remove ELASTICSEARCH.IN.SH", USAGE);
}
public void testOfficialPluginName_ThrowsException() throws IOException {
PluginManager.checkForOfficialPlugins("analysis-icu");
PluginManager.checkForOfficialPlugins("analysis-kuromoji");
PluginManager.checkForOfficialPlugins("analysis-phonetic");
PluginManager.checkForOfficialPlugins("analysis-smartcn");
PluginManager.checkForOfficialPlugins("analysis-stempel");
PluginManager.checkForOfficialPlugins("delete-by-query");
PluginManager.checkForOfficialPlugins("lang-javascript");
PluginManager.checkForOfficialPlugins("lang-painless");
PluginManager.checkForOfficialPlugins("lang-python");
PluginManager.checkForOfficialPlugins("mapper-attachments");
PluginManager.checkForOfficialPlugins("mapper-murmur3");
PluginManager.checkForOfficialPlugins("mapper-size");
PluginManager.checkForOfficialPlugins("discovery-multicast");
PluginManager.checkForOfficialPlugins("discovery-azure");
PluginManager.checkForOfficialPlugins("discovery-ec2");
PluginManager.checkForOfficialPlugins("discovery-gce");
PluginManager.checkForOfficialPlugins("repository-azure");
PluginManager.checkForOfficialPlugins("repository-s3");
PluginManager.checkForOfficialPlugins("store-smb");
try {
PluginManager.checkForOfficialPlugins("elasticsearch-mapper-attachment");
fail("elasticsearch-mapper-attachment should not be allowed");
} catch (IllegalArgumentException e) {
// We expect that error
}
}
public void testThatBasicAuthIsRejectedOnHttp() throws Exception {
assertStatus(String.format(Locale.ROOT, "install http://user:pass@localhost:12345/foo.zip --verbose"), CliTool.ExitStatus.IO_ERROR);
assertThat(terminal.getTerminalOutput(), hasItem(containsString("Basic auth is only supported for HTTPS!")));
}
public void testThatBasicAuthIsSupportedWithHttps() throws Exception {
assumeTrue("test requires security manager to be disabled", System.getSecurityManager() == null);
SSLSocketFactory defaultSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
ServerBootstrap serverBootstrap = new ServerBootstrap(new NioServerSocketChannelFactory());
SelfSignedCertificate ssc = null;
try {
try {
ssc = new SelfSignedCertificate("localhost");
} catch (Exception e) {
assumeNoException("self signing shenanigans not supported by this JDK", e);
}
// Create a trust manager that does not validate certificate chains:
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, InsecureTrustManagerFactory.INSTANCE.getTrustManagers(), null);
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
final List<HttpRequest> requests = new ArrayList<>();
final SslContext sslContext = SslContext.newServerContext(ssc.certificate(), ssc.privateKey());
serverBootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
return Channels.pipeline(
new SslHandler(sslContext.newEngine()),
new HttpRequestDecoder(),
new HttpResponseEncoder(),
new LoggingServerHandler(requests)
);
}
});
Channel channel = serverBootstrap.bind(new InetSocketAddress(InetAddress.getByName("localhost"), 0));
int port = ((InetSocketAddress) channel.getLocalAddress()).getPort();
// IO_ERROR because there is no real file delivered...
assertStatus(String.format(Locale.ROOT, "install https://user:pass@localhost:%s/foo.zip --verbose --timeout 10s", port), ExitStatus.IO_ERROR);
// ensure that we did not try any other data source like download.elastic.co, in case we specified our own local URL
assertThat(terminal.getTerminalOutput(), not(hasItem(containsString("download.elastic.co"))));
assertThat(requests, hasSize(1));
String msg = String.format(Locale.ROOT, "Request header did not contain Authorization header, terminal output was: %s", terminal.getTerminalOutput());
assertThat(msg, requests.get(0).headers().contains("Authorization"), is(true));
assertThat(msg, requests.get(0).headers().get("Authorization"), is("Basic " + Base64.encodeBytes("user:pass".getBytes(StandardCharsets.UTF_8))));
} finally {
HttpsURLConnection.setDefaultSSLSocketFactory(defaultSocketFactory);
serverBootstrap.releaseExternalResources();
if (ssc != null) {
ssc.delete();
}
}
}
private static class LoggingServerHandler extends SimpleChannelUpstreamHandler {
private List<HttpRequest> requests;
public LoggingServerHandler(List<HttpRequest> requests) {
this.requests = requests;
}
@Override
public void messageReceived(final ChannelHandlerContext ctx, final MessageEvent e) throws InterruptedException {
final HttpRequest request = (HttpRequest) e.getMessage();
requests.add(request);
final org.jboss.netty.handler.codec.http.HttpResponse response = new DefaultHttpResponse(HTTP_1_1, HttpResponseStatus.BAD_REQUEST);
ctx.getChannel().write(response);
}
}
private Environment buildInitialSettings() throws IOException {
Settings settings = settingsBuilder()
.put("http.enabled", true)
.put(Environment.PATH_HOME_SETTING.getKey(), createTempDir()).build();
return InternalSettingsPreparer.prepareEnvironment(settings, null);
}
private void assertStatusOk(String command) {
assertStatus(command, ExitStatus.OK);
}
private void assertStatus(String command, ExitStatus exitStatus) {
ExitStatus status = new PluginManagerCliParser(terminal).execute(args(command));
assertThat("Terminal output was: " + terminal.getTerminalOutput(), status, is(exitStatus));
}
private void assertThatPluginIsListed(String pluginName) {
terminal.getTerminalOutput().clear();
assertStatusOk("list");
String message = String.format(Locale.ROOT, "Terminal output was: %s", terminal.getTerminalOutput());
assertThat(message, terminal.getTerminalOutput(), hasItem(containsString(pluginName)));
}
private void assertThatPluginIsNotListed(String pluginName) {
terminal.getTerminalOutput().clear();
assertStatusOk("list");
String message = String.format(Locale.ROOT, "Terminal output was: %s", terminal.getTerminalOutput());
assertFalse(message, terminal.getTerminalOutput().contains(pluginName));
}
}

View File

@ -1,135 +0,0 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import org.elasticsearch.Build;
import org.elasticsearch.Version;
import org.elasticsearch.common.SuppressForbidden;
import org.elasticsearch.common.http.client.HttpDownloadHelper;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
import org.junit.After;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Locale;
import static org.elasticsearch.common.settings.Settings.settingsBuilder;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
/**
*
*/
@SuppressForbidden(reason = "modifies system properties intentionally")
public class PluginManagerUnitTests extends ESTestCase {
@After
public void cleanSystemProperty() {
System.clearProperty(PluginManager.PROPERTY_SUPPORT_STAGING_URLS);
}
public void testThatConfigDirectoryCanBeOutsideOfElasticsearchHomeDirectory() throws IOException {
String pluginName = randomAsciiOfLength(10);
Path homeFolder = createTempDir();
Path genericConfigFolder = createTempDir();
Settings settings = settingsBuilder()
.put(Environment.PATH_CONF_SETTING.getKey(), genericConfigFolder)
.put(Environment.PATH_HOME_SETTING.getKey(), homeFolder)
.build();
Environment environment = new Environment(settings);
PluginManager.PluginHandle pluginHandle = new PluginManager.PluginHandle(pluginName, "version", "user");
Path configDirPath = pluginHandle.configDir(environment).normalize();
Path expectedDirPath = genericConfigFolder.resolve(pluginName).normalize();
assertEquals(configDirPath, expectedDirPath);
}
public void testSimplifiedNaming() throws IOException {
String pluginName = randomAsciiOfLength(10);
PluginManager.PluginHandle handle = PluginManager.PluginHandle.parse(pluginName);
boolean supportStagingUrls = randomBoolean();
if (supportStagingUrls) {
System.setProperty(PluginManager.PROPERTY_SUPPORT_STAGING_URLS, "true");
}
Iterator<URL> iterator = handle.urls().iterator();
if (supportStagingUrls) {
String expectedStagingURL = String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/staging/%s-%s/org/elasticsearch/plugin/%s/%s/%s-%s.zip",
Version.CURRENT.number(), Build.CURRENT.shortHash(), pluginName, Version.CURRENT.number(), pluginName, Version.CURRENT.number());
assertThat(iterator.next().toExternalForm(), is(expectedStagingURL));
}
URL expected = new URL("https", "download.elastic.co", "/elasticsearch/release/org/elasticsearch/plugin/" + pluginName + "/" + Version.CURRENT.number() + "/" +
pluginName + "-" + Version.CURRENT.number() + ".zip");
assertThat(iterator.next().toExternalForm(), is(expected.toExternalForm()));
assertThat(iterator.hasNext(), is(false));
}
public void testOfficialPluginName() throws IOException {
String randomPluginName = randomFrom(new ArrayList<>(PluginManager.OFFICIAL_PLUGINS));
PluginManager.PluginHandle handle = PluginManager.PluginHandle.parse(randomPluginName);
assertThat(handle.name, is(randomPluginName));
boolean supportStagingUrls = randomBoolean();
if (supportStagingUrls) {
System.setProperty(PluginManager.PROPERTY_SUPPORT_STAGING_URLS, "true");
}
Iterator<URL> iterator = handle.urls().iterator();
if (supportStagingUrls) {
String expectedStagingUrl = String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/staging/%s-%s/org/elasticsearch/plugin/%s/%s/%s-%s.zip",
Version.CURRENT.number(), Build.CURRENT.shortHash(), randomPluginName, Version.CURRENT.number(), randomPluginName, Version.CURRENT.number());
assertThat(iterator.next().toExternalForm(), is(expectedStagingUrl));
}
String releaseUrl = String.format(Locale.ROOT, "https://download.elastic.co/elasticsearch/release/org/elasticsearch/plugin/%s/%s/%s-%s.zip",
randomPluginName, Version.CURRENT.number(), randomPluginName, Version.CURRENT.number());
assertThat(iterator.next().toExternalForm(), is(releaseUrl));
assertThat(iterator.hasNext(), is(false));
}
public void testGithubPluginName() throws IOException {
String user = randomAsciiOfLength(6);
String pluginName = randomAsciiOfLength(10);
PluginManager.PluginHandle handle = PluginManager.PluginHandle.parse(user + "/" + pluginName);
assertThat(handle.name, is(pluginName));
assertThat(handle.urls(), hasSize(1));
assertThat(handle.urls().get(0).toExternalForm(), is(new URL("https", "github.com", "/" + user + "/" + pluginName + "/" + "archive/master.zip").toExternalForm()));
}
public void testDownloadHelperChecksums() throws Exception {
// Sanity check to make sure the checksum functions never change how they checksum things
assertEquals("0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
HttpDownloadHelper.SHA1_CHECKSUM.checksum("foo".getBytes(Charset.forName("UTF-8"))));
assertEquals("acbd18db4cc2f85cedef654fccc4a4d8",
HttpDownloadHelper.MD5_CHECKSUM.checksum("foo".getBytes(Charset.forName("UTF-8"))));
}
}

View File

@ -0,0 +1,111 @@
/*
* 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.
*/
package org.elasticsearch.plugins;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.lucene.util.LuceneTestCase;
import org.elasticsearch.common.cli.CliTool;
import org.elasticsearch.common.cli.CliToolTestCase;
import org.elasticsearch.common.cli.Terminal;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.test.ESTestCase;
@LuceneTestCase.SuppressFileSystems("*")
public class RemovePluginCommandTests extends ESTestCase {
/** Creates a test environment with bin, config and plugins directories. */
static Environment createEnv() throws IOException {
Path home = createTempDir();
Files.createDirectories(home.resolve("bin"));
Files.createFile(home.resolve("bin").resolve("elasticsearch"));
Files.createDirectories(home.resolve("plugins"));
Settings settings = Settings.builder()
.put("path.home", home)
.build();
return new Environment(settings);
}
static CliToolTestCase.CaptureOutputTerminal removePlugin(String name, Environment env) throws Exception {
CliToolTestCase.CaptureOutputTerminal terminal = new CliToolTestCase.CaptureOutputTerminal(Terminal.Verbosity.VERBOSE);
CliTool.ExitStatus status = new RemovePluginCommand(terminal, name).execute(env.settings(), env);
assertEquals(CliTool.ExitStatus.OK, status);
return terminal;
}
static void assertRemoveCleaned(Environment env) throws IOException {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(env.pluginsFile())) {
for (Path file : stream) {
if (file.getFileName().toString().startsWith(".removing")) {
fail("Removal dir still exists, " + file);
}
}
}
}
public void testMissing() throws Exception {
Environment env = createEnv();
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
removePlugin("dne", env);
});
assertTrue(e.getMessage(), e.getMessage().contains("Plugin dne not found"));
assertRemoveCleaned(env);
}
public void testBasic() throws Exception {
Environment env = createEnv();
Files.createDirectory(env.pluginsFile().resolve("fake"));
Files.createFile(env.pluginsFile().resolve("fake").resolve("plugin.jar"));
Files.createDirectory(env.pluginsFile().resolve("fake").resolve("subdir"));
Files.createDirectory(env.pluginsFile().resolve("other"));
removePlugin("fake", env);
assertFalse(Files.exists(env.pluginsFile().resolve("fake")));
assertTrue(Files.exists(env.pluginsFile().resolve("other")));
assertRemoveCleaned(env);
}
public void testBin() throws Exception {
Environment env = createEnv();
Files.createDirectories(env.pluginsFile().resolve("fake"));
Path binDir = env.binFile().resolve("fake");
Files.createDirectories(binDir);
Files.createFile(binDir.resolve("somescript"));
removePlugin("fake", env);
assertFalse(Files.exists(env.pluginsFile().resolve("fake")));
assertTrue(Files.exists(env.binFile().resolve("elasticsearch")));
assertFalse(Files.exists(binDir));
assertRemoveCleaned(env);
}
public void testBinNotDir() throws Exception {
Environment env = createEnv();
Files.createDirectories(env.pluginsFile().resolve("elasticsearch"));
IllegalStateException e = expectThrows(IllegalStateException.class, () -> {
removePlugin("elasticsearch", env);
});
assertTrue(Files.exists(env.pluginsFile().resolve("elasticsearch"))); // did not remove
assertTrue(Files.exists(env.binFile().resolve("elasticsearch")));
assertRemoveCleaned(env);
}
}

View File

@ -64,8 +64,6 @@ public abstract class CliToolTestCase extends ESTestCase {
*/
public static class MockTerminal extends Terminal {
private static final PrintWriter DEV_NULL = new PrintWriter(new DevNullWriter());
public MockTerminal() {
super(Verbosity.NORMAL);
}
@ -93,29 +91,7 @@ public abstract class CliToolTestCase extends ESTestCase {
}
@Override
public void printStackTrace(Throwable t) {
return;
}
@Override
public PrintWriter writer() {
return DEV_NULL;
}
private static class DevNullWriter extends Writer {
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
}
@Override
public void flush() throws IOException {
}
@Override
public void close() throws IOException {
}
}
public void printStackTrace(Throwable t) {}
}
/**