mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-03-24 17:09:48 +00:00
Merge pull request #16336 from rjernst/demangle
Reduce complexity of plugin cli
This commit is contained in:
commit
661e3c99f7
@ -50,7 +50,7 @@ public class HelpPrinter {
|
||||
}
|
||||
});
|
||||
} catch (IOException ioe) {
|
||||
ioe.printStackTrace(terminal.writer());
|
||||
throw new RuntimeException(ioe);
|
||||
}
|
||||
terminal.println();
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
124
core/src/main/java/org/elasticsearch/plugins/PluginCli.java
Normal file
124
core/src/main/java/org/elasticsearch/plugins/PluginCli.java
Normal 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]);
|
||||
}
|
||||
}
|
@ -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 + "]");
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
|
Binary file not shown.
@ -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...
|
||||
}
|
@ -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"));
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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"))));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
Loading…
x
Reference in New Issue
Block a user