Make xpack extensible:
* Add XPackExtension: an api class (like Plugin in core) for what a x-pack extension can do. * Add XPackExtensionCli: a cli tool for adding, removing and listing extensions for x-pack. * Add XPackExtensionService: loading of jars from pluginsdir/x-pack/extensions, into child classloader. * Add bin/x-pack/extension script, similar to plugin cli, which installs an extension into pluginsdir/x-pack/extensions. * Add XPack extension integration test. Fixed elastic/elasticsearch#1515 Original commit: elastic/x-pack-elasticsearch@130ba03270
This commit is contained in:
parent
c5d155efe9
commit
9c6aa6353e
|
@ -1,19 +1,38 @@
|
||||||
apply plugin: 'elasticsearch.esplugin'
|
import org.elasticsearch.gradle.MavenFilteringHack
|
||||||
|
import org.elasticsearch.gradle.VersionProperties
|
||||||
|
|
||||||
esplugin {
|
apply plugin: 'elasticsearch.build'
|
||||||
description 'a very basic implementation of a custom realm to validate it works'
|
|
||||||
classname 'org.elasticsearch.example.ExampleRealmPlugin'
|
|
||||||
isolated false
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
provided "org.elasticsearch:elasticsearch:${versions.elasticsearch}"
|
||||||
|
testCompile "org.elasticsearch.test:framework:${project.versions.elasticsearch}"
|
||||||
provided project(path: ':x-plugins:elasticsearch:x-pack', configuration: 'runtime')
|
provided project(path: ':x-plugins:elasticsearch:x-pack', configuration: 'runtime')
|
||||||
}
|
}
|
||||||
|
|
||||||
compileJava.options.compilerArgs << "-Xlint:-rawtypes"
|
Map generateSubstitutions() {
|
||||||
//compileTestJava.options.compilerArgs << "-Xlint:-rawtypes"
|
def stringSnap = { version ->
|
||||||
|
if (version.endsWith("-SNAPSHOT")) {
|
||||||
|
return version.substring(0, version.length() - 9)
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'version': stringSnap(version),
|
||||||
|
'xpack.version': stringSnap(VersionProperties.elasticsearch),
|
||||||
|
'java.version': targetCompatibility as String
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
integTest {
|
processResources {
|
||||||
|
MavenFilteringHack.filter(it, generateSubstitutions())
|
||||||
|
}
|
||||||
|
|
||||||
|
task buildZip(type:Zip, dependsOn: [jar]) {
|
||||||
|
from 'build/resources/main/xpack-extension-descriptor.properties'
|
||||||
|
from project.jar
|
||||||
|
}
|
||||||
|
|
||||||
|
task integTest(type: org.elasticsearch.gradle.test.RestIntegTestTask, dependsOn: buildZip) {
|
||||||
cluster {
|
cluster {
|
||||||
plugin 'x-pack', project(':x-plugins:elasticsearch:x-pack')
|
plugin 'x-pack', project(':x-plugins:elasticsearch:x-pack')
|
||||||
// TODO: these should be settings?
|
// TODO: these should be settings?
|
||||||
|
@ -24,6 +43,8 @@ integTest {
|
||||||
|
|
||||||
setupCommand 'setupDummyUser',
|
setupCommand 'setupDummyUser',
|
||||||
'bin/xpack/esusers', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin'
|
'bin/xpack/esusers', 'useradd', 'test_user', '-p', 'changeme', '-r', 'admin'
|
||||||
|
setupCommand 'installExtension',
|
||||||
|
'bin/xpack/extension', 'install', 'file:' + buildZip.archivePath
|
||||||
waitCondition = { node, ant ->
|
waitCondition = { node, ant ->
|
||||||
File tmpFile = new File(node.cwd, 'wait.success')
|
File tmpFile = new File(node.cwd, 'wait.success')
|
||||||
ant.get(src: "http://${node.httpUri()}",
|
ant.get(src: "http://${node.httpUri()}",
|
||||||
|
@ -36,4 +57,5 @@ integTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
check.dependsOn integTest
|
||||||
|
integTest.mustRunAfter precommit
|
|
@ -8,11 +8,10 @@ package org.elasticsearch.example;
|
||||||
import org.elasticsearch.example.realm.CustomAuthenticationFailureHandler;
|
import org.elasticsearch.example.realm.CustomAuthenticationFailureHandler;
|
||||||
import org.elasticsearch.example.realm.CustomRealm;
|
import org.elasticsearch.example.realm.CustomRealm;
|
||||||
import org.elasticsearch.example.realm.CustomRealmFactory;
|
import org.elasticsearch.example.realm.CustomRealmFactory;
|
||||||
import org.elasticsearch.plugins.Plugin;
|
|
||||||
import org.elasticsearch.shield.authc.AuthenticationModule;
|
import org.elasticsearch.shield.authc.AuthenticationModule;
|
||||||
|
import org.elasticsearch.xpack.extensions.XPackExtension;
|
||||||
|
|
||||||
public class ExampleRealmPlugin extends Plugin {
|
public class ExampleRealmExtension extends XPackExtension {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String name() {
|
public String name() {
|
||||||
return "custom realm example";
|
return "custom realm example";
|
|
@ -0,0 +1,6 @@
|
||||||
|
description=Custom Realm Extension
|
||||||
|
version=${version}
|
||||||
|
name=examplerealm
|
||||||
|
classname=org.elasticsearch.example.ExampleRealmExtension
|
||||||
|
java.version=${java.version}
|
||||||
|
xpack.version=${xpack.version}
|
|
@ -13,6 +13,7 @@ import org.elasticsearch.client.transport.TransportClient;
|
||||||
import org.elasticsearch.common.settings.Settings;
|
import org.elasticsearch.common.settings.Settings;
|
||||||
import org.elasticsearch.common.transport.TransportAddress;
|
import org.elasticsearch.common.transport.TransportAddress;
|
||||||
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
import org.elasticsearch.common.util.concurrent.ThreadContext;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
import org.elasticsearch.plugins.Plugin;
|
import org.elasticsearch.plugins.Plugin;
|
||||||
import org.elasticsearch.test.ESIntegTestCase;
|
import org.elasticsearch.test.ESIntegTestCase;
|
||||||
import org.elasticsearch.test.rest.client.http.HttpResponse;
|
import org.elasticsearch.test.rest.client.http.HttpResponse;
|
||||||
|
@ -64,6 +65,7 @@ public class CustomRealmIT extends ESIntegTestCase {
|
||||||
|
|
||||||
Settings settings = Settings.builder()
|
Settings settings = Settings.builder()
|
||||||
.put("cluster.name", clusterName)
|
.put("cluster.name", clusterName)
|
||||||
|
.put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toAbsolutePath().toString())
|
||||||
.put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER)
|
.put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER)
|
||||||
.put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW)
|
.put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW)
|
||||||
.build();
|
.build();
|
||||||
|
@ -83,6 +85,7 @@ public class CustomRealmIT extends ESIntegTestCase {
|
||||||
|
|
||||||
Settings settings = Settings.builder()
|
Settings settings = Settings.builder()
|
||||||
.put("cluster.name", clusterName)
|
.put("cluster.name", clusterName)
|
||||||
|
.put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toAbsolutePath().toString())
|
||||||
.put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER + randomAsciiOfLength(1))
|
.put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, CustomRealm.KNOWN_USER + randomAsciiOfLength(1))
|
||||||
.put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW)
|
.put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, CustomRealm.KNOWN_PW)
|
||||||
.build();
|
.build();
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
# you may not use this file except in compliance with the Elastic License.
|
||||||
|
|
||||||
|
SCRIPT="$0"
|
||||||
|
|
||||||
|
# SCRIPT may be an arbitrarily deep series of symlinks. Loop until we have the concrete path.
|
||||||
|
while [ -h "$SCRIPT" ] ; do
|
||||||
|
ls=`ls -ld "$SCRIPT"`
|
||||||
|
# Drop everything prior to ->
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
SCRIPT="$link"
|
||||||
|
else
|
||||||
|
SCRIPT=`dirname "$SCRIPT"`/"$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# determine elasticsearch home
|
||||||
|
ES_HOME=`dirname "$SCRIPT"`/../..
|
||||||
|
|
||||||
|
# make ELASTICSEARCH_HOME absolute
|
||||||
|
ES_HOME=`cd "$ES_HOME"; pwd`
|
||||||
|
|
||||||
|
# If an include wasn't specified in the environment, then search for one...
|
||||||
|
if [ "x$ES_INCLUDE" = "x" ]; then
|
||||||
|
# Locations (in order) to use when searching for an include file.
|
||||||
|
for include in /usr/share/elasticsearch/elasticsearch.in.sh \
|
||||||
|
/usr/local/share/elasticsearch/elasticsearch.in.sh \
|
||||||
|
/opt/elasticsearch/elasticsearch.in.sh \
|
||||||
|
~/.elasticsearch.in.sh \
|
||||||
|
"`dirname "$0"`"/../elasticsearch.in.sh \
|
||||||
|
"$ES_HOME/bin/elasticsearch.in.sh"; do
|
||||||
|
if [ -r "$include" ]; then
|
||||||
|
. "$include"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# ...otherwise, source the specified include.
|
||||||
|
elif [ -r "$ES_INCLUDE" ]; then
|
||||||
|
. "$ES_INCLUDE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -x "$JAVA_HOME/bin/java" ]; then
|
||||||
|
JAVA="$JAVA_HOME/bin/java"
|
||||||
|
else
|
||||||
|
JAVA=`which java`
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$JAVA" ]; then
|
||||||
|
echo "Could not find any executable java binary. Please install java in your PATH or set JAVA_HOME"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$ES_CLASSPATH" ]; then
|
||||||
|
echo "You must set the ES_CLASSPATH var" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to read package config files
|
||||||
|
if [ -f "/etc/sysconfig/elasticsearch" ]; then
|
||||||
|
CONF_DIR=/etc/elasticsearch
|
||||||
|
CONF_FILE=$CONF_DIR/elasticsearch.yml
|
||||||
|
|
||||||
|
. "/etc/sysconfig/elasticsearch"
|
||||||
|
elif [ -f "/etc/default/elasticsearch" ]; then
|
||||||
|
CONF_DIR=/etc/elasticsearch
|
||||||
|
CONF_FILE=$CONF_DIR/elasticsearch.yml
|
||||||
|
|
||||||
|
. "/etc/default/elasticsearch"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse any long getopt options and put them into properties before calling getopt below
|
||||||
|
# Be dash compatible to make sure running under ubuntu works
|
||||||
|
ARGCOUNT=$#
|
||||||
|
COUNT=0
|
||||||
|
while [ $COUNT -lt $ARGCOUNT ]
|
||||||
|
do
|
||||||
|
case $1 in
|
||||||
|
--*=*) properties="$properties -Des.${1#--}"
|
||||||
|
shift 1; COUNT=$(($COUNT+1))
|
||||||
|
;;
|
||||||
|
--*) properties="$properties -Des.${1#--}=$2"
|
||||||
|
shift ; shift; COUNT=$(($COUNT+2))
|
||||||
|
;;
|
||||||
|
*) set -- "$@" "$1"; shift; COUNT=$(($COUNT+1))
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# check if properties already has a config file or config dir
|
||||||
|
if [ -e "$CONF_DIR" ]; then
|
||||||
|
case "$properties" in
|
||||||
|
*-Des.default.path.conf=*) ;;
|
||||||
|
*)
|
||||||
|
if [ ! -d "$CONF_DIR/xpack" ]; then
|
||||||
|
echo "ERROR: The configuration directory [$CONF_DIR/xpack] does not exist. The extension tool expects security configuration files in that location."
|
||||||
|
echo "The plugin may not have been installed with the correct configuration path. If [$ES_HOME/config/xpack] exists, please copy the 'xpack' directory to [$CONF_DIR]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
properties="$properties -Des.default.path.conf=$CONF_DIR"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
export HOSTNAME=`hostname -s`
|
||||||
|
|
||||||
|
# include x-pack jars in classpath
|
||||||
|
ES_CLASSPATH="$ES_CLASSPATH:$ES_HOME/plugins/xpack/*"
|
||||||
|
|
||||||
|
cd "$ES_HOME" > /dev/null
|
||||||
|
"$JAVA" $ES_JAVA_OPTS -cp "$ES_CLASSPATH" -Des.path.home="$ES_HOME" $properties org.elasticsearch.xpack.extensions.XPackExtensionCli "$@"
|
||||||
|
status=$?
|
||||||
|
cd - > /dev/null
|
||||||
|
exit $status
|
|
@ -0,0 +1,9 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
rem Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
rem or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
rem you may not use this file except in compliance with the Elastic License.
|
||||||
|
|
||||||
|
PUSHD "%~dp0"
|
||||||
|
CALL "%~dp0.in.bat" org.elasticsearch.xpack.extensions.XPackExtensionCli %*
|
||||||
|
POPD
|
|
@ -7,12 +7,10 @@ package org.elasticsearch.xpack;
|
||||||
|
|
||||||
import org.elasticsearch.client.Client;
|
import org.elasticsearch.client.Client;
|
||||||
import org.elasticsearch.shield.authc.support.SecuredString;
|
import org.elasticsearch.shield.authc.support.SecuredString;
|
||||||
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
|
|
||||||
import org.elasticsearch.shield.client.SecurityClient;
|
import org.elasticsearch.shield.client.SecurityClient;
|
||||||
import org.elasticsearch.watcher.client.WatcherClient;
|
import org.elasticsearch.watcher.client.WatcherClient;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.BASIC_AUTH_HEADER;
|
import static org.elasticsearch.shield.authc.support.UsernamePasswordToken.BASIC_AUTH_HEADER;
|
||||||
|
|
|
@ -22,15 +22,19 @@ import org.elasticsearch.marvel.Marvel;
|
||||||
import org.elasticsearch.plugins.Plugin;
|
import org.elasticsearch.plugins.Plugin;
|
||||||
import org.elasticsearch.script.ScriptModule;
|
import org.elasticsearch.script.ScriptModule;
|
||||||
import org.elasticsearch.shield.Shield;
|
import org.elasticsearch.shield.Shield;
|
||||||
|
import org.elasticsearch.shield.authc.AuthenticationModule;
|
||||||
import org.elasticsearch.watcher.Watcher;
|
import org.elasticsearch.watcher.Watcher;
|
||||||
import org.elasticsearch.xpack.common.init.LazyInitializationModule;
|
import org.elasticsearch.xpack.common.init.LazyInitializationModule;
|
||||||
import org.elasticsearch.xpack.common.init.LazyInitializationService;
|
import org.elasticsearch.xpack.common.init.LazyInitializationService;
|
||||||
|
import org.elasticsearch.xpack.extensions.XPackExtension;
|
||||||
|
import org.elasticsearch.xpack.extensions.XPackExtensionsService;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.AccessController;
|
import java.security.AccessController;
|
||||||
import java.security.PrivilegedAction;
|
import java.security.PrivilegedAction;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
public class XPackPlugin extends Plugin {
|
public class XPackPlugin extends Plugin {
|
||||||
|
|
||||||
|
@ -67,6 +71,7 @@ public class XPackPlugin extends Plugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final Settings settings;
|
protected final Settings settings;
|
||||||
|
protected final XPackExtensionsService extensionsService;
|
||||||
|
|
||||||
protected Licensing licensing;
|
protected Licensing licensing;
|
||||||
protected Shield shield;
|
protected Shield shield;
|
||||||
|
@ -81,6 +86,14 @@ public class XPackPlugin extends Plugin {
|
||||||
this.marvel = new Marvel(settings);
|
this.marvel = new Marvel(settings);
|
||||||
this.watcher = new Watcher(settings);
|
this.watcher = new Watcher(settings);
|
||||||
this.graph = new Graph(settings);
|
this.graph = new Graph(settings);
|
||||||
|
// Check if the node is a transport client.
|
||||||
|
if (transportClientMode(settings) == false) {
|
||||||
|
Environment env = new Environment(settings);
|
||||||
|
this.extensionsService =
|
||||||
|
new XPackExtensionsService(settings, resolveXPackExtensionsFile(env), getExtensions());
|
||||||
|
} else {
|
||||||
|
this.extensionsService = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public String name() {
|
@Override public String name() {
|
||||||
|
@ -91,6 +104,11 @@ public class XPackPlugin extends Plugin {
|
||||||
return "Elastic X-Pack";
|
return "Elastic X-Pack";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For tests only
|
||||||
|
public Collection<Class<? extends XPackExtension>> getExtensions() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<Module> nodeModules() {
|
public Collection<Module> nodeModules() {
|
||||||
ArrayList<Module> modules = new ArrayList<>();
|
ArrayList<Module> modules = new ArrayList<>();
|
||||||
|
@ -157,6 +175,12 @@ public class XPackPlugin extends Plugin {
|
||||||
graph.onModule(module);
|
graph.onModule(module);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void onModule(AuthenticationModule module) {
|
||||||
|
if (extensionsService != null) {
|
||||||
|
extensionsService.onModule(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void onIndexModule(IndexModule module) {
|
public void onIndexModule(IndexModule module) {
|
||||||
shield.onIndexModule(module);
|
shield.onIndexModule(module);
|
||||||
graph.onIndexModule(module);
|
graph.onIndexModule(module);
|
||||||
|
@ -221,4 +245,8 @@ public class XPackPlugin extends Plugin {
|
||||||
settingsModule.registerSetting(Setting.boolSetting(legacyFeatureEnabledSetting(featureName),
|
settingsModule.registerSetting(Setting.boolSetting(legacyFeatureEnabledSetting(featureName),
|
||||||
defaultValue, Setting.Property.NodeScope));
|
defaultValue, Setting.Property.NodeScope));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Path resolveXPackExtensionsFile(Environment env) {
|
||||||
|
return env.pluginsFile().resolve("xpack").resolve("extensions");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import joptsimple.OptionSet;
|
||||||
|
import joptsimple.OptionSpec;
|
||||||
|
import org.apache.lucene.util.IOUtils;
|
||||||
|
|
||||||
|
import org.elasticsearch.bootstrap.JarHell;
|
||||||
|
import org.elasticsearch.cli.Command;
|
||||||
|
import org.elasticsearch.cli.ExitCodes;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.cli.UserError;
|
||||||
|
import org.elasticsearch.common.io.FileSystemUtils;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLDecoder;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
|
||||||
|
import static org.elasticsearch.xpack.XPackPlugin.resolveXPackExtensionsFile;
|
||||||
|
import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command for the extension cli to install an extension into x-pack.
|
||||||
|
*
|
||||||
|
* The install command takes a URL to an extension zip.
|
||||||
|
*
|
||||||
|
* Extensions are packaged as zip files. Each packaged extension must contain an
|
||||||
|
* extension properties file. See {@link XPackExtensionInfo}.
|
||||||
|
* <p>
|
||||||
|
* The installation process first extracts the extensions files into a temporary
|
||||||
|
* directory in order to verify the extension satisfies the following requirements:
|
||||||
|
* <ul>
|
||||||
|
* <li>The property file exists and contains valid metadata. See {@link XPackExtensionInfo#readFromProperties(Path)}</li>
|
||||||
|
* <li>Jar hell does not exist, either between the extension's own jars or with the parent classloader (elasticsearch + xpack)</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
class InstallXPackExtensionCommand extends Command {
|
||||||
|
|
||||||
|
private final Environment env;
|
||||||
|
private final OptionSpec<Void> batchOption;
|
||||||
|
private final OptionSpec<String> arguments;
|
||||||
|
|
||||||
|
InstallXPackExtensionCommand(Environment env) {
|
||||||
|
super("Install a plugin");
|
||||||
|
this.env = env;
|
||||||
|
this.batchOption = parser.acceptsAll(Arrays.asList("b", "batch"),
|
||||||
|
"Enable batch mode explicitly, automatic confirmation of security permission");
|
||||||
|
this.arguments = parser.nonOptions("plugin id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(Terminal terminal, OptionSet options) throws Exception {
|
||||||
|
// TODO: in jopt-simple 5.0 we can enforce a min/max number of positional args
|
||||||
|
List<String> args = arguments.values(options);
|
||||||
|
if (args.size() != 1) {
|
||||||
|
throw new UserError(ExitCodes.USAGE, "Must supply a single extension id argument");
|
||||||
|
}
|
||||||
|
String extensionURL = args.get(0);
|
||||||
|
boolean isBatch = options.has(batchOption) || System.console() == null;
|
||||||
|
execute(terminal, extensionURL, isBatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// pkg private for testing
|
||||||
|
void execute(Terminal terminal, String extensionId, boolean isBatch) throws Exception {
|
||||||
|
if (Files.exists(resolveXPackExtensionsFile(env)) == false) {
|
||||||
|
terminal.println("xpack extensions directory [" + resolveXPackExtensionsFile(env) + "] does not exist. Creating...");
|
||||||
|
Files.createDirectories(resolveXPackExtensionsFile(env));
|
||||||
|
}
|
||||||
|
|
||||||
|
Path extensionZip = download(terminal, extensionId, env.tmpFile());
|
||||||
|
Path extractedZip = unzip(extensionZip, resolveXPackExtensionsFile(env));
|
||||||
|
install(terminal, extractedZip, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Downloads the extension and returns the file it was downloaded to. */
|
||||||
|
private Path download(Terminal terminal, String extensionURL, Path tmpDir) throws Exception {
|
||||||
|
terminal.println("-> Downloading " + URLDecoder.decode(extensionURL, "UTF-8"));
|
||||||
|
URL url = new URL(extensionURL);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path unzip(Path zip, Path extensionDir) throws IOException, UserError {
|
||||||
|
// unzip extension to a staging temp dir
|
||||||
|
Path target = Files.createTempDirectory(extensionDir, ".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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Files.delete(zip);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load information about the extension, and verify it can be installed with no errors. */
|
||||||
|
private XPackExtensionInfo verify(Terminal terminal, Path extensionRoot, Environment env) throws Exception {
|
||||||
|
// read and validate the extension descriptor
|
||||||
|
XPackExtensionInfo info = XPackExtensionInfo.readFromProperties(extensionRoot);
|
||||||
|
terminal.println(VERBOSE, info.toString());
|
||||||
|
|
||||||
|
// check for jar hell before any copying
|
||||||
|
jarHellCheck(extensionRoot);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** check a candidate extension for jar hell before installing it */
|
||||||
|
private void jarHellCheck(Path candidate) throws Exception {
|
||||||
|
// create list of current jars in classpath
|
||||||
|
// including the x-pack jars (see $ES_CLASSPATH in bin/extension script)
|
||||||
|
final List<URL> jars = new ArrayList<>();
|
||||||
|
jars.addAll(Arrays.asList(JarHell.parseClassPath()));
|
||||||
|
|
||||||
|
// add extension jars to the list
|
||||||
|
Path extensionJars[] = FileSystemUtils.files(candidate, "*.jar");
|
||||||
|
for (Path jar : extensionJars) {
|
||||||
|
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 extension from {@code tmpRoot} into the extensions dir.
|
||||||
|
*/
|
||||||
|
private void install(Terminal terminal, Path tmpRoot, Environment env) throws Exception {
|
||||||
|
List<Path> deleteOnFailure = new ArrayList<>();
|
||||||
|
deleteOnFailure.add(tmpRoot);
|
||||||
|
try {
|
||||||
|
XPackExtensionInfo info = verify(terminal, tmpRoot, env);
|
||||||
|
final Path destination = resolveXPackExtensionsFile(env).resolve(info.getName());
|
||||||
|
if (Files.exists(destination)) {
|
||||||
|
throw new UserError(ExitCodes.USAGE,
|
||||||
|
"extension directory " + destination.toAbsolutePath() +
|
||||||
|
" already exists. To update the extension, uninstall it first using 'remove " +
|
||||||
|
info.getName() + "' command");
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import joptsimple.OptionSet;
|
||||||
|
|
||||||
|
import org.elasticsearch.cli.Command;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.elasticsearch.xpack.XPackPlugin.resolveXPackExtensionsFile;
|
||||||
|
import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command for the extension cli to list extensions installed in x-pack.
|
||||||
|
*/
|
||||||
|
class ListXPackExtensionCommand extends Command {
|
||||||
|
private final Environment env;
|
||||||
|
|
||||||
|
ListXPackExtensionCommand(Environment env) {
|
||||||
|
super("Lists installed x-pack extensions");
|
||||||
|
this.env = env;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(Terminal terminal, OptionSet options) throws Exception {
|
||||||
|
if (Files.exists(resolveXPackExtensionsFile(env)) == false) {
|
||||||
|
throw new IOException("Extensions directory missing: " + resolveXPackExtensionsFile(env));
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.println(VERBOSE, "Extensions directory: " + resolveXPackExtensionsFile(env));
|
||||||
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(resolveXPackExtensionsFile(env))) {
|
||||||
|
for (Path extension : stream) {
|
||||||
|
terminal.println(extension.getFileName().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import joptsimple.OptionSet;
|
||||||
|
import joptsimple.OptionSpec;
|
||||||
|
|
||||||
|
import org.apache.lucene.util.IOUtils;
|
||||||
|
import org.elasticsearch.cli.Command;
|
||||||
|
import org.elasticsearch.cli.ExitCodes;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.cli.UserError;
|
||||||
|
import org.elasticsearch.common.Strings;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.elasticsearch.xpack.XPackPlugin.resolveXPackExtensionsFile;
|
||||||
|
import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command for the extension cli to remove an extension from x-pack.
|
||||||
|
*/
|
||||||
|
class RemoveXPackExtensionCommand extends Command {
|
||||||
|
private final Environment env;
|
||||||
|
private final OptionSpec<String> arguments;
|
||||||
|
|
||||||
|
RemoveXPackExtensionCommand(Environment env) {
|
||||||
|
super("Removes an extension from x-pack");
|
||||||
|
this.env = env;
|
||||||
|
this.arguments = parser.nonOptions("extension name");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void execute(Terminal terminal, OptionSet options) throws Exception {
|
||||||
|
// TODO: in jopt-simple 5.0 we can enforce a min/max number of positional args
|
||||||
|
List<String> args = arguments.values(options);
|
||||||
|
if (args.size() != 1) {
|
||||||
|
throw new UserError(ExitCodes.USAGE, "Must supply a single extension id argument");
|
||||||
|
}
|
||||||
|
execute(terminal, args.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// pkg private for testing
|
||||||
|
void execute(Terminal terminal, String extensionName) throws Exception {
|
||||||
|
terminal.println("-> Removing " + Strings.coalesceToEmpty(extensionName) + "...");
|
||||||
|
|
||||||
|
Path extensionDir = resolveXPackExtensionsFile(env).resolve(extensionName);
|
||||||
|
if (Files.exists(extensionDir) == false) {
|
||||||
|
throw new UserError(ExitCodes.USAGE,
|
||||||
|
"Extension " + extensionName + " not found. Run 'bin/xpack/extension list' to get list of installed extensions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Path> extensionPaths = new ArrayList<>();
|
||||||
|
|
||||||
|
terminal.println(VERBOSE, "Removing: " + extensionDir);
|
||||||
|
Path tmpExtensionDir = resolveXPackExtensionsFile(env).resolve(".removing-" + extensionName);
|
||||||
|
Files.move(extensionDir, tmpExtensionDir, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
extensionPaths.add(tmpExtensionDir);
|
||||||
|
|
||||||
|
IOUtils.rm(extensionPaths.toArray(new Path[extensionPaths.size()]));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import org.elasticsearch.shield.authc.AuthenticationModule;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An extension point allowing to plug in custom functionality in x-pack authentication module.
|
||||||
|
*/
|
||||||
|
public abstract class XPackExtension {
|
||||||
|
/**
|
||||||
|
* The name of the plugin.
|
||||||
|
*/
|
||||||
|
public abstract String name();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The description of the plugin.
|
||||||
|
*/
|
||||||
|
public abstract String description();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement this function to register custom extensions in the authentication module.
|
||||||
|
*/
|
||||||
|
public void onModule(AuthenticationModule module) {}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import org.apache.log4j.BasicConfigurator;
|
||||||
|
import org.apache.log4j.varia.NullAppender;
|
||||||
|
import org.elasticsearch.cli.MultiCommand;
|
||||||
|
import org.elasticsearch.cli.Terminal;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
import org.elasticsearch.node.internal.InternalSettingsPreparer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cli tool for adding, removing and listing extensions for x-pack.
|
||||||
|
*/
|
||||||
|
public class XPackExtensionCli extends MultiCommand {
|
||||||
|
|
||||||
|
public XPackExtensionCli(Environment env) {
|
||||||
|
super("A tool for managing installed x-pack extensions");
|
||||||
|
subcommands.put("list", new ListXPackExtensionCommand(env));
|
||||||
|
subcommands.put("install", new InstallXPackExtensionCommand(env));
|
||||||
|
subcommands.put("remove", new RemoveXPackExtensionCommand(env));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
BasicConfigurator.configure(new NullAppender());
|
||||||
|
Environment env = InternalSettingsPreparer.prepareEnvironment(Settings.EMPTY, Terminal.DEFAULT);
|
||||||
|
exit(new XPackExtensionCli(env).main(args, Terminal.DEFAULT));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import org.elasticsearch.Version;
|
||||||
|
import org.elasticsearch.bootstrap.JarHell;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class XPackExtensionInfo {
|
||||||
|
public static final String XPACK_EXTENSION_PROPERTIES = "xpack-extension-descriptor.properties";
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private String version;
|
||||||
|
private String classname;
|
||||||
|
|
||||||
|
public XPackExtensionInfo() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about extensions
|
||||||
|
*
|
||||||
|
* @param name Its name
|
||||||
|
* @param description Its description
|
||||||
|
* @param version Version number
|
||||||
|
*/
|
||||||
|
XPackExtensionInfo(String name, String description, String version, String classname) {
|
||||||
|
this.name = name;
|
||||||
|
this.description = description;
|
||||||
|
this.version = version;
|
||||||
|
this.classname = classname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** reads (and validates) extension metadata descriptor file */
|
||||||
|
public static XPackExtensionInfo readFromProperties(Path dir) throws IOException {
|
||||||
|
Path descriptor = dir.resolve(XPACK_EXTENSION_PROPERTIES);
|
||||||
|
Properties props = new Properties();
|
||||||
|
try (InputStream stream = Files.newInputStream(descriptor)) {
|
||||||
|
props.load(stream);
|
||||||
|
}
|
||||||
|
String name = props.getProperty("name");
|
||||||
|
if (name == null || name.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Property [name] is missing in [" + descriptor + "]");
|
||||||
|
}
|
||||||
|
String description = props.getProperty("description");
|
||||||
|
if (description == null) {
|
||||||
|
throw new IllegalArgumentException("Property [description] is missing for extension [" + name + "]");
|
||||||
|
}
|
||||||
|
String version = props.getProperty("version");
|
||||||
|
if (version == null) {
|
||||||
|
throw new IllegalArgumentException("Property [version] is missing for extension [" + name + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
String xpackVersionString = props.getProperty("xpack.version");
|
||||||
|
if (xpackVersionString == null) {
|
||||||
|
throw new IllegalArgumentException("Property [xpack.version] is missing for extension [" + name + "]");
|
||||||
|
}
|
||||||
|
Version xpackVersion = Version.fromString(xpackVersionString);
|
||||||
|
if (xpackVersion.equals(Version.CURRENT) == false) {
|
||||||
|
throw new IllegalArgumentException("extension [" + name + "] is incompatible with Elasticsearch [" +
|
||||||
|
Version.CURRENT.toString() + "]. Was designed for version [" + xpackVersionString + "]");
|
||||||
|
}
|
||||||
|
String javaVersionString = props.getProperty("java.version");
|
||||||
|
if (javaVersionString == null) {
|
||||||
|
throw new IllegalArgumentException("Property [java.version] is missing for extension [" + name + "]");
|
||||||
|
}
|
||||||
|
JarHell.checkVersionFormat(javaVersionString);
|
||||||
|
JarHell.checkJavaVersion(name, javaVersionString);
|
||||||
|
String classname = props.getProperty("classname");
|
||||||
|
if (classname == null) {
|
||||||
|
throw new IllegalArgumentException("Property [classname] is missing for extension [" + name + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new XPackExtensionInfo(name, description, version, classname);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Extension's name
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Extension's description if any
|
||||||
|
*/
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return extension's classname
|
||||||
|
*/
|
||||||
|
public String getClassname() {
|
||||||
|
return classname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Version number for the extension
|
||||||
|
*/
|
||||||
|
public String getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
final StringBuilder information = new StringBuilder()
|
||||||
|
.append("- XPack Extension information:\n")
|
||||||
|
.append("Name: ").append(name).append("\n")
|
||||||
|
.append("Description: ").append(description).append("\n")
|
||||||
|
.append("Version: ").append(version).append("\n")
|
||||||
|
.append(" * Classname: ").append(classname);
|
||||||
|
|
||||||
|
return information.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import org.elasticsearch.ElasticsearchException;
|
||||||
|
import org.elasticsearch.bootstrap.JarHell;
|
||||||
|
import org.elasticsearch.common.collect.Tuple;
|
||||||
|
import org.elasticsearch.common.io.FileSystemUtils;
|
||||||
|
import org.elasticsearch.common.logging.ESLogger;
|
||||||
|
import org.elasticsearch.common.logging.Loggers;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.shield.authc.AuthenticationModule;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLClassLoader;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.elasticsearch.common.io.FileSystemUtils.isAccessibleDirectory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class XPackExtensionsService {
|
||||||
|
private final Settings settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We keep around a list of extensions
|
||||||
|
*/
|
||||||
|
private final List<Tuple<XPackExtensionInfo, XPackExtension> > extensions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new XPackExtensionsService
|
||||||
|
* @param settings The settings of the system
|
||||||
|
* @param extsDirectory The directory extensions exist in, or null if extensions should not be loaded from the filesystem
|
||||||
|
* @param classpathExtensions Extensions that exist in the classpath which should be loaded
|
||||||
|
*/
|
||||||
|
public XPackExtensionsService(Settings settings, Path extsDirectory, Collection<Class<? extends XPackExtension>> classpathExtensions) {
|
||||||
|
this.settings = settings;
|
||||||
|
List<Tuple<XPackExtensionInfo, XPackExtension>> extensionsLoaded = new ArrayList<>();
|
||||||
|
|
||||||
|
// first we load extensions that are on the classpath. this is for tests
|
||||||
|
for (Class<? extends XPackExtension> extClass : classpathExtensions) {
|
||||||
|
XPackExtension ext = loadExtension(extClass, settings);
|
||||||
|
XPackExtensionInfo extInfo = new XPackExtensionInfo(ext.name(), ext.description(), "NA", extClass.getName());
|
||||||
|
extensionsLoaded.add(new Tuple<>(extInfo, ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
// now, find all the ones that are in plugins/xpack/extensions
|
||||||
|
if (extsDirectory != null) {
|
||||||
|
try {
|
||||||
|
List<Bundle> bundles = getExtensionBundles(extsDirectory);
|
||||||
|
List<Tuple<XPackExtensionInfo, XPackExtension>> loaded = loadBundles(bundles);
|
||||||
|
extensionsLoaded.addAll(loaded);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new IllegalStateException("Unable to initialize extensions", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extensions = Collections.unmodifiableList(extensionsLoaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onModule(AuthenticationModule module) {
|
||||||
|
for (Tuple<XPackExtensionInfo, XPackExtension> tuple : extensions) {
|
||||||
|
tuple.v2().onModule(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a "bundle" is a an extension in a single classloader.
|
||||||
|
static class Bundle {
|
||||||
|
XPackExtensionInfo info;
|
||||||
|
List<URL> urls = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Bundle> getExtensionBundles(Path extsDirectory) throws IOException {
|
||||||
|
ESLogger logger = Loggers.getLogger(XPackExtensionsService.class);
|
||||||
|
|
||||||
|
// TODO: remove this leniency, but tests bogusly rely on it
|
||||||
|
if (!isAccessibleDirectory(extsDirectory, logger)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Bundle> bundles = new ArrayList<>();
|
||||||
|
|
||||||
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(extsDirectory)) {
|
||||||
|
for (Path extension : stream) {
|
||||||
|
if (FileSystemUtils.isHidden(extension)) {
|
||||||
|
logger.trace("--- skip hidden extension file[{}]", extension.toAbsolutePath());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logger.trace("--- adding extension [{}]", extension.toAbsolutePath());
|
||||||
|
final XPackExtensionInfo info;
|
||||||
|
try {
|
||||||
|
info = XPackExtensionInfo.readFromProperties(extension);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IllegalStateException("Could not load extension descriptor for existing extension ["
|
||||||
|
+ extension.getFileName() + "]. Was the extension built before 2.0?", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<URL> urls = new ArrayList<>();
|
||||||
|
try (DirectoryStream<Path> jarStream = Files.newDirectoryStream(extension, "*.jar")) {
|
||||||
|
for (Path jar : jarStream) {
|
||||||
|
// normalize with toRealPath to get symlinks out of our hair
|
||||||
|
urls.add(jar.toRealPath().toUri().toURL());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final Bundle bundle = new Bundle();
|
||||||
|
bundles.add(bundle);
|
||||||
|
bundle.info = info;
|
||||||
|
bundle.urls.addAll(urls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Tuple<XPackExtensionInfo, XPackExtension> > loadBundles(List<Bundle> bundles) {
|
||||||
|
List<Tuple<XPackExtensionInfo, XPackExtension>> exts = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Bundle bundle : bundles) {
|
||||||
|
// jar-hell check the bundle against the parent classloader and the x-pack classloader
|
||||||
|
// pluginmanager does it, but we do it again, in case lusers mess with jar files manually
|
||||||
|
try {
|
||||||
|
final List<URL> jars = new ArrayList<>();
|
||||||
|
// add the parent jars to the list
|
||||||
|
jars.addAll(Arrays.asList(JarHell.parseClassPath()));
|
||||||
|
|
||||||
|
// add the x-pack jars to the list
|
||||||
|
ClassLoader xpackLoader = getClass().getClassLoader();
|
||||||
|
// this class is loaded from the isolated x-pack plugin's classloader
|
||||||
|
if (xpackLoader instanceof URLClassLoader) {
|
||||||
|
jars.addAll(Arrays.asList(((URLClassLoader) xpackLoader).getURLs()));
|
||||||
|
}
|
||||||
|
|
||||||
|
jars.addAll(bundle.urls);
|
||||||
|
|
||||||
|
JarHell.checkJarHell(jars.toArray(new URL[0]));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("failed to load bundle " + bundle.urls + " due to jar hell", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a child to load the extension in this bundle
|
||||||
|
ClassLoader loader = URLClassLoader.newInstance(bundle.urls.toArray(new URL[0]), getClass().getClassLoader());
|
||||||
|
final Class<? extends XPackExtension> extClass = loadExtensionClass(bundle.info.getClassname(), loader);
|
||||||
|
final XPackExtension ext = loadExtension(extClass, settings);
|
||||||
|
exts.add(new Tuple<>(bundle.info, ext));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Collections.unmodifiableList(exts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Class<? extends XPackExtension> loadExtensionClass(String className, ClassLoader loader) {
|
||||||
|
try {
|
||||||
|
return loader.loadClass(className).asSubclass(XPackExtension.class);
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
throw new ElasticsearchException("Could not find extension class [" + className + "]", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private XPackExtension loadExtension(Class<? extends XPackExtension> extClass, Settings settings) {
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
return extClass.getConstructor(Settings.class).newInstance(settings);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
try {
|
||||||
|
return extClass.getConstructor().newInstance();
|
||||||
|
} catch (NoSuchMethodException e1) {
|
||||||
|
throw new ElasticsearchException("No constructor for [" + extClass + "]. An extension class must " +
|
||||||
|
"have either an empty default constructor or a single argument constructor accepting a " +
|
||||||
|
"Settings instance");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
throw new ElasticsearchException("Failed to load extension class [" + extClass.getName() + "]", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import org.apache.lucene.util.LuceneTestCase;
|
||||||
|
import org.elasticsearch.Version;
|
||||||
|
import org.elasticsearch.cli.MockTerminal;
|
||||||
|
import org.elasticsearch.cli.UserError;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.FileVisitResult;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
@LuceneTestCase.SuppressFileSystems("*")
|
||||||
|
public class InstallXPackExtensionCommandTests extends ESTestCase {
|
||||||
|
/**
|
||||||
|
* Creates a test environment with plugins and xpack extensions directories.
|
||||||
|
*/
|
||||||
|
static Environment createEnv() throws IOException {
|
||||||
|
Path home = createTempDir();
|
||||||
|
Files.createDirectories(home.resolve("org/elasticsearch/xpack/extensions").resolve("xpack").resolve("extensions"));
|
||||||
|
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 {
|
||||||
|
String target = structure.relativize(file).toString();
|
||||||
|
stream.putNextEntry(new ZipEntry(target));
|
||||||
|
Files.copy(file, stream);
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return zip.toUri().toURL().toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates an extension .zip and returns the url for testing
|
||||||
|
*/
|
||||||
|
static String createExtension(String name, Path structure) throws IOException {
|
||||||
|
XPackExtensionTestUtil.writeProperties(structure,
|
||||||
|
"description", "fake desc",
|
||||||
|
"name", name,
|
||||||
|
"version", "1.0",
|
||||||
|
"xpack.version", Version.CURRENT.toString(),
|
||||||
|
"java.version", System.getProperty("java.specification.version"),
|
||||||
|
"classname", "FakeExtension");
|
||||||
|
writeJar(structure.resolve("extension.jar"), "FakeExtension");
|
||||||
|
return writeZip(structure);
|
||||||
|
}
|
||||||
|
|
||||||
|
static MockTerminal installExtension(String extensionUrl, Environment env) throws Exception {
|
||||||
|
MockTerminal terminal = new MockTerminal();
|
||||||
|
new InstallXPackExtensionCommand(env).execute(terminal, extensionUrl, true);
|
||||||
|
return terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertExtension(String name, Path original, Environment env) throws IOException {
|
||||||
|
Path got = env.pluginsFile().resolve("xpack").resolve("extensions").resolve(name);
|
||||||
|
assertTrue("dir " + name + " exists", Files.exists(got));
|
||||||
|
assertTrue("jar was copied", Files.exists(got.resolve("extension.jar")));
|
||||||
|
assertInstallCleaned(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
void assertInstallCleaned(Environment env) throws IOException {
|
||||||
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(env.pluginsFile().resolve("xpack").resolve("extensions"))) {
|
||||||
|
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 extDir = createTempDir();
|
||||||
|
String extZip = createExtension("fake", extDir);
|
||||||
|
installExtension(extZip, env);
|
||||||
|
assertExtension("fake", extDir, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testSpaceInUrl() throws Exception {
|
||||||
|
Environment env = createEnv();
|
||||||
|
Path extDir = createTempDir();
|
||||||
|
String extZip = createExtension("fake", extDir);
|
||||||
|
Path extZipWithSpaces = createTempFile("foo bar", ".zip");
|
||||||
|
try (InputStream in = new URL(extZip).openStream()) {
|
||||||
|
Files.copy(in, extZipWithSpaces, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
installExtension(extZipWithSpaces.toUri().toURL().toString(), env);
|
||||||
|
assertExtension("fake", extDir, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testMalformedUrlNotMaven() throws Exception {
|
||||||
|
// has two colons, so it appears similar to maven coordinates
|
||||||
|
MalformedURLException e = expectThrows(MalformedURLException.class, () -> {
|
||||||
|
installExtension("://host:1234", createEnv());
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage(), e.getMessage().contains("no protocol"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testJarHell() throws Exception {
|
||||||
|
Environment env = createEnv();
|
||||||
|
Path extDir = createTempDir();
|
||||||
|
writeJar(extDir.resolve("other.jar"), "FakeExtension");
|
||||||
|
String extZip = createExtension("fake", extDir); // adds extension.jar with FakeExtension
|
||||||
|
IllegalStateException e = expectThrows(IllegalStateException.class, () -> {
|
||||||
|
installExtension(extZip, env);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage(), e.getMessage().contains("jar hell"));
|
||||||
|
assertInstallCleaned(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testIsolatedExtension() throws Exception {
|
||||||
|
Environment env = createEnv();
|
||||||
|
// these both share the same FakeExtension class
|
||||||
|
Path extDir1 = createTempDir();
|
||||||
|
String extZip1 = createExtension("fake1", extDir1);
|
||||||
|
installExtension(extZip1, env);
|
||||||
|
Path extDir2 = createTempDir();
|
||||||
|
String extZip2 = createExtension("fake2", extDir2);
|
||||||
|
installExtension(extZip2, env);
|
||||||
|
assertExtension("fake1", extDir1, env);
|
||||||
|
assertExtension("fake2", extDir2, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testExistingExtension() throws Exception {
|
||||||
|
Environment env = createEnv();
|
||||||
|
String extZip = createExtension("fake", createTempDir());
|
||||||
|
installExtension(extZip, env);
|
||||||
|
UserError e = expectThrows(UserError.class, () -> {
|
||||||
|
installExtension(extZip, env);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage(), e.getMessage().contains("already exists"));
|
||||||
|
assertInstallCleaned(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testMissingDescriptor() throws Exception {
|
||||||
|
Environment env = createEnv();
|
||||||
|
Path extDir = createTempDir();
|
||||||
|
Files.createFile(extDir.resolve("fake.yml"));
|
||||||
|
String extZip = writeZip(extDir);
|
||||||
|
NoSuchFileException e = expectThrows(NoSuchFileException.class, () -> {
|
||||||
|
installExtension(extZip, env);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage(), e.getMessage().contains("xpack-extension-descriptor.properties"));
|
||||||
|
assertInstallCleaned(env);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import org.apache.lucene.util.LuceneTestCase;
|
||||||
|
import org.elasticsearch.cli.ExitCodes;
|
||||||
|
import org.elasticsearch.cli.MockTerminal;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
@LuceneTestCase.SuppressFileSystems("*")
|
||||||
|
public class ListXPackExtensionCommandTests extends ESTestCase {
|
||||||
|
|
||||||
|
Environment createEnv() throws IOException {
|
||||||
|
Path home = createTempDir();
|
||||||
|
Settings settings = Settings.builder()
|
||||||
|
.put("path.home", home)
|
||||||
|
.build();
|
||||||
|
return new Environment(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path createExtensionDir(Environment env) throws IOException {
|
||||||
|
Path path = env.pluginsFile().resolve("xpack").resolve("extensions");
|
||||||
|
return Files.createDirectories(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static MockTerminal listExtensions(Environment env) throws Exception {
|
||||||
|
MockTerminal terminal = new MockTerminal();
|
||||||
|
String[] args = {};
|
||||||
|
int status = new ListXPackExtensionCommand(env).main(args, terminal);
|
||||||
|
assertEquals(ExitCodes.OK, status);
|
||||||
|
return terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testExtensionsDirMissing() throws Exception {
|
||||||
|
Environment env = createEnv();
|
||||||
|
Path extDir = createExtensionDir(env);
|
||||||
|
Files.delete(extDir);
|
||||||
|
IOException e = expectThrows(IOException.class, () -> {
|
||||||
|
listExtensions(env);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage(), e.getMessage().contains("Extensions directory missing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testNoExtensions() throws Exception {
|
||||||
|
Environment env = createEnv();
|
||||||
|
createExtensionDir(env);
|
||||||
|
MockTerminal terminal = listExtensions(env);
|
||||||
|
assertTrue(terminal.getOutput(), terminal.getOutput().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testOneExtension() throws Exception {
|
||||||
|
Environment env = createEnv();
|
||||||
|
Path extDir = createExtensionDir(env);
|
||||||
|
Files.createDirectory(extDir.resolve("fake"));
|
||||||
|
MockTerminal terminal = listExtensions(env);
|
||||||
|
assertTrue(terminal.getOutput(), terminal.getOutput().contains("fake"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testTwoExtensions() throws Exception {
|
||||||
|
Environment env = createEnv();
|
||||||
|
Path extDir = createExtensionDir(env);
|
||||||
|
Files.createDirectory(extDir.resolve("fake1"));
|
||||||
|
Files.createDirectory(extDir.resolve("fake2"));
|
||||||
|
MockTerminal terminal = listExtensions(env);
|
||||||
|
String output = terminal.getOutput();
|
||||||
|
assertTrue(output, output.contains("fake1"));
|
||||||
|
assertTrue(output, output.contains("fake2"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import org.apache.lucene.util.LuceneTestCase;
|
||||||
|
import org.elasticsearch.cli.MockTerminal;
|
||||||
|
import org.elasticsearch.cli.UserError;
|
||||||
|
import org.elasticsearch.common.settings.Settings;
|
||||||
|
import org.elasticsearch.env.Environment;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
@LuceneTestCase.SuppressFileSystems("*")
|
||||||
|
public class RemoveXPackExtensionCommandTests extends ESTestCase {
|
||||||
|
|
||||||
|
/** Creates a test environment with bin, config and plugins directories. */
|
||||||
|
static Environment createEnv() throws IOException {
|
||||||
|
Path home = createTempDir();
|
||||||
|
Settings settings = Settings.builder()
|
||||||
|
.put("path.home", home)
|
||||||
|
.build();
|
||||||
|
return new Environment(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path createExtensionDir(Environment env) throws IOException {
|
||||||
|
Path path = env.pluginsFile().resolve("xpack").resolve("extensions");
|
||||||
|
return Files.createDirectories(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static MockTerminal removeExtension(String name, Environment env) throws Exception {
|
||||||
|
MockTerminal terminal = new MockTerminal();
|
||||||
|
new RemoveXPackExtensionCommand(env).execute(terminal, name);
|
||||||
|
return terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void assertRemoveCleaned(Path extDir) throws IOException {
|
||||||
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(extDir)) {
|
||||||
|
for (Path file : stream) {
|
||||||
|
if (file.getFileName().toString().startsWith(".removing")) {
|
||||||
|
fail("Removal dir still exists, " + file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testMissing() throws Exception {
|
||||||
|
Environment env = createEnv();
|
||||||
|
Path extDir = createExtensionDir(env);
|
||||||
|
UserError e = expectThrows(UserError.class, () -> {
|
||||||
|
removeExtension("dne", env);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage(), e.getMessage().contains("Extension dne not found"));
|
||||||
|
assertRemoveCleaned(extDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testBasic() throws Exception {
|
||||||
|
Environment env = createEnv();
|
||||||
|
Path extDir = createExtensionDir(env);
|
||||||
|
Files.createDirectory(extDir.resolve("fake"));
|
||||||
|
Files.createFile(extDir.resolve("fake").resolve("extension.jar"));
|
||||||
|
Files.createDirectory(extDir.resolve("fake").resolve("subdir"));
|
||||||
|
Files.createDirectory(extDir.resolve("other"));
|
||||||
|
removeExtension("fake", env);
|
||||||
|
assertFalse(Files.exists(extDir.resolve("fake")));
|
||||||
|
assertTrue(Files.exists(extDir.resolve("other")));
|
||||||
|
assertRemoveCleaned(extDir);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import org.elasticsearch.Version;
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class XPackExtensionInfoTests extends ESTestCase {
|
||||||
|
|
||||||
|
public void testReadFromProperties() throws Exception {
|
||||||
|
Path extensionDir = createTempDir().resolve("fake-extension");
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir,
|
||||||
|
"description", "fake desc",
|
||||||
|
"name", "my_extension",
|
||||||
|
"version", "1.0",
|
||||||
|
"xpack.version", Version.CURRENT.toString(),
|
||||||
|
"java.version", System.getProperty("java.specification.version"),
|
||||||
|
"classname", "FakeExtension");
|
||||||
|
XPackExtensionInfo info = XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
assertEquals("my_extension", info.getName());
|
||||||
|
assertEquals("fake desc", info.getDescription());
|
||||||
|
assertEquals("1.0", info.getVersion());
|
||||||
|
assertEquals("FakeExtension", info.getClassname());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReadFromPropertiesNameMissing() throws Exception {
|
||||||
|
Path extensionDir = createTempDir().resolve("fake-extension");
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir);
|
||||||
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
|
||||||
|
XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage().contains("Property [name] is missing in"));
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir, "name", "");
|
||||||
|
IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, () -> {
|
||||||
|
XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
});
|
||||||
|
assertTrue(e1.getMessage().contains("Property [name] is missing in"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReadFromPropertiesDescriptionMissing() throws Exception {
|
||||||
|
Path extensionDir = createTempDir().resolve("fake-extension");
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir, "name", "fake-extension");
|
||||||
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
|
||||||
|
XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage().contains("[description] is missing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReadFromPropertiesVersionMissing() throws Exception {
|
||||||
|
Path extensionDir = createTempDir().resolve("fake-extension");
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir, "description", "fake desc", "name", "fake-extension");
|
||||||
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
|
||||||
|
XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage().contains("[version] is missing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReadFromPropertiesElasticsearchVersionMissing() throws Exception {
|
||||||
|
Path extensionDir = createTempDir().resolve("fake-extension");
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir,
|
||||||
|
"description", "fake desc",
|
||||||
|
"name", "my_extension",
|
||||||
|
"version", "1.0");
|
||||||
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
|
||||||
|
XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage().contains("[xpack.version] is missing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReadFromPropertiesJavaVersionMissing() throws Exception {
|
||||||
|
Path extensionDir = createTempDir().resolve("fake-extension");
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir,
|
||||||
|
"description", "fake desc",
|
||||||
|
"name", "my_extension",
|
||||||
|
"xpack.version", Version.CURRENT.toString(),
|
||||||
|
"version", "1.0");
|
||||||
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
|
||||||
|
XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage().contains("[java.version] is missing"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReadFromPropertiesJavaVersionIncompatible() throws Exception {
|
||||||
|
String extensionName = "fake-extension";
|
||||||
|
Path extensionDir = createTempDir().resolve(extensionName);
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir,
|
||||||
|
"description", "fake desc",
|
||||||
|
"name", extensionName,
|
||||||
|
"xpack.version", Version.CURRENT.toString(),
|
||||||
|
"java.version", "1000000.0",
|
||||||
|
"classname", "FakeExtension",
|
||||||
|
"version", "1.0");
|
||||||
|
IllegalStateException e = expectThrows(IllegalStateException.class, () -> {
|
||||||
|
XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage(), e.getMessage().contains(extensionName + " requires Java"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReadFromPropertiesBadJavaVersionFormat() throws Exception {
|
||||||
|
String extensionName = "fake-extension";
|
||||||
|
Path extensionDir = createTempDir().resolve(extensionName);
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir,
|
||||||
|
"description", "fake desc",
|
||||||
|
"name", extensionName,
|
||||||
|
"xpack.version", Version.CURRENT.toString(),
|
||||||
|
"java.version", "1.7.0_80",
|
||||||
|
"classname", "FakeExtension",
|
||||||
|
"version", "1.0");
|
||||||
|
IllegalStateException e = expectThrows(IllegalStateException.class, () -> {
|
||||||
|
XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage(),
|
||||||
|
e.getMessage().equals("version string must be a sequence of nonnegative decimal " +
|
||||||
|
"integers separated by \".\"'s and may have leading zeros but was 1.7.0_80"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReadFromPropertiesBogusElasticsearchVersion() throws Exception {
|
||||||
|
Path extensionDir = createTempDir().resolve("fake-extension");
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir,
|
||||||
|
"description", "fake desc",
|
||||||
|
"version", "1.0",
|
||||||
|
"name", "my_extension",
|
||||||
|
"xpack.version", "bogus");
|
||||||
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
|
||||||
|
XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage().contains("version needs to contain major, minor, and revision"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReadFromPropertiesOldElasticsearchVersion() throws Exception {
|
||||||
|
Path extensionDir = createTempDir().resolve("fake-extension");
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir,
|
||||||
|
"description", "fake desc",
|
||||||
|
"name", "my_extension",
|
||||||
|
"version", "1.0",
|
||||||
|
"xpack.version", Version.V_2_0_0.toString());
|
||||||
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
|
||||||
|
XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage().contains("Was designed for version [2.0.0]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testReadFromPropertiesJvmMissingClassname() throws Exception {
|
||||||
|
Path extensionDir = createTempDir().resolve("fake-extension");
|
||||||
|
XPackExtensionTestUtil.writeProperties(extensionDir,
|
||||||
|
"description", "fake desc",
|
||||||
|
"name", "my_extension",
|
||||||
|
"version", "1.0",
|
||||||
|
"xpack.version", Version.CURRENT.toString(),
|
||||||
|
"java.version", System.getProperty("java.specification.version"));
|
||||||
|
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> {
|
||||||
|
XPackExtensionInfo.readFromProperties(extensionDir);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage().contains("Property [classname] is missing"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/** Utility methods for testing extensions */
|
||||||
|
public class XPackExtensionTestUtil {
|
||||||
|
|
||||||
|
/** convenience method to write a plugin properties file */
|
||||||
|
public static void writeProperties(Path pluginDir, String... stringProps) throws IOException {
|
||||||
|
assert stringProps.length % 2 == 0;
|
||||||
|
Files.createDirectories(pluginDir);
|
||||||
|
Path propertiesFile = pluginDir.resolve(XPackExtensionInfo.XPACK_EXTENSION_PROPERTIES);
|
||||||
|
Properties properties = new Properties();
|
||||||
|
for (int i = 0; i < stringProps.length; i += 2) {
|
||||||
|
properties.put(stringProps[i], stringProps[i + 1]);
|
||||||
|
}
|
||||||
|
try (OutputStream out = Files.newOutputStream(propertiesFile)) {
|
||||||
|
properties.store(out, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||||
|
* or more contributor license agreements. Licensed under the Elastic License;
|
||||||
|
* you may not use this file except in compliance with the Elastic License.
|
||||||
|
*/
|
||||||
|
package org.elasticsearch.xpack.extensions;
|
||||||
|
|
||||||
|
import org.elasticsearch.test.ESTestCase;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class XPackExtensionsServiceTests extends ESTestCase {
|
||||||
|
public void testExistingPluginMissingDescriptor() throws Exception {
|
||||||
|
Path extensionsDir = createTempDir();
|
||||||
|
Files.createDirectory(extensionsDir.resolve("extension-missing-descriptor"));
|
||||||
|
IllegalStateException e = expectThrows(IllegalStateException.class, () -> {
|
||||||
|
XPackExtensionsService.getExtensionBundles(extensionsDir);
|
||||||
|
});
|
||||||
|
assertTrue(e.getMessage(),
|
||||||
|
e.getMessage().contains("Could not load extension descriptor for existing extension"));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue