mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-06-27 17:10:22 -04:00
Introduce declarative plugin management (#77544)
Closes #70219. Introduce a declarative way for the Elasticsearch server to manage plugins, which reads the `elasticsearch-plugins.yml` file and works which out plugins need to be added and / or removed to match the configuration. Also make it possible to configure a proxy in the config file, instead of through the environment. Most of the work of adding and removing is still done in the `InstallPluginAction` and `RemovePluginAction` classes, so the behaviour should be the same as with the `install` and `remove` commands. However, these commands will now abort if the above config file exists. The intent is to make it harder for the configuration to drift. This new method only applies to `docker` distribution types at the moment. Since this syncing mechanism declarative, rather than imperative, the Cloud-specific plugin wrapper script is no longer required. Instead, an environment variable informs `InstallPluginAction` to install plugins from an archive directory instead of downloading them, where possible.
This commit is contained in:
parent
63d0d66a07
commit
3018e52335
48 changed files with 2071 additions and 381 deletions
|
@ -13,6 +13,7 @@
|
|||
* build/distributions/local
|
||||
* */
|
||||
import org.elasticsearch.gradle.Architecture
|
||||
import org.elasticsearch.gradle.VersionProperties
|
||||
|
||||
// gradle has an open issue of failing applying plugins in
|
||||
// precompiled script plugins (see https://github.com/gradle/gradle/issues/17004)
|
||||
|
@ -29,6 +30,6 @@ tasks.register('localDistro', Sync) {
|
|||
from(elasticsearch_distributions.local)
|
||||
into("build/distribution/local")
|
||||
doLast {
|
||||
logger.lifecycle("Elasticsearch distribution installed to ${destinationDir}.")
|
||||
logger.lifecycle("Elasticsearch distribution installed to ${destinationDir}/elasticsearch-${VersionProperties.elasticsearch}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ public class DistroTestPlugin implements Plugin<Project> {
|
|||
Map<String, TaskProvider<?>> versionTasks = versionTasks(project, "destructiveDistroUpgradeTest");
|
||||
TaskProvider<Task> destructiveDistroTest = project.getTasks().register("destructiveDistroTest");
|
||||
|
||||
// Configuration examplePlugin = configureExamplePlugin(project);
|
||||
Configuration examplePlugin = configureExamplePlugin(project);
|
||||
|
||||
List<TaskProvider<Test>> windowsTestTasks = new ArrayList<>();
|
||||
Map<ElasticsearchDistributionType, List<TaskProvider<Test>>> linuxTestTasks = new HashMap<>();
|
||||
|
@ -114,11 +114,12 @@ public class DistroTestPlugin implements Plugin<Project> {
|
|||
TaskProvider<?> depsTask = project.getTasks().register(taskname + "#deps");
|
||||
// explicitly depend on the archive not on the implicit extracted distribution
|
||||
depsTask.configure(t -> t.dependsOn(distribution.getArchiveDependencies()));
|
||||
depsTask.configure(t -> t.dependsOn(examplePlugin.getDependencies()));
|
||||
depsTasks.put(taskname, depsTask);
|
||||
TaskProvider<Test> destructiveTask = configureTestTask(project, taskname, distribution, t -> {
|
||||
t.onlyIf(t2 -> distribution.isDocker() == false || dockerSupport.get().getDockerAvailability().isAvailable);
|
||||
addDistributionSysprop(t, DISTRIBUTION_SYSPROP, distribution::getFilepath);
|
||||
// addDistributionSysprop(t, EXAMPLE_PLUGIN_SYSPROP, () -> examplePlugin.getSingleFile().toString());
|
||||
addDistributionSysprop(t, EXAMPLE_PLUGIN_SYSPROP, () -> examplePlugin.getSingleFile().toString());
|
||||
t.exclude("**/PackageUpgradeTests.class");
|
||||
}, depsTask);
|
||||
|
||||
|
@ -314,7 +315,7 @@ public class DistroTestPlugin implements Plugin<Project> {
|
|||
Configuration examplePlugin = project.getConfigurations().create(EXAMPLE_PLUGIN_CONFIGURATION);
|
||||
examplePlugin.getAttributes().attribute(ArtifactAttributes.ARTIFACT_FORMAT, ArtifactTypeDefinition.ZIP_TYPE);
|
||||
DependencyHandler deps = project.getDependencies();
|
||||
deps.add(EXAMPLE_PLUGIN_CONFIGURATION, deps.create("org.elasticsearch.examples:custom-settings:1.0.0-SNAPSHOT"));
|
||||
deps.add(EXAMPLE_PLUGIN_CONFIGURATION, deps.project(Map.of("path", ":plugins:analysis-icu", "configuration", "zip")));
|
||||
return examplePlugin;
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<message key="javadoc.missing" value="Types should explain their purpose" />
|
||||
</module>
|
||||
|
||||
<!-- Public methods must have JavaDoc -->
|
||||
<!-- Check the Javadoc for a method e.g that it has the correct parameters, return type etc -->
|
||||
<module name="JavadocMethod">
|
||||
<property name="severity" value="warning"/>
|
||||
<property name="accessModifiers" value="public"/>
|
||||
|
|
|
@ -296,7 +296,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
|
|||
*****************************************************************************/
|
||||
libFiles =
|
||||
copySpec {
|
||||
// delay by using closures, since they have not yet been configured, so no jar task exists yet
|
||||
// Delay by using closures, since they have not yet been configured, so no jar task exists yet.
|
||||
from(configurations.libs)
|
||||
into('tools/geoip-cli') {
|
||||
from(configurations.libsGeoIpCli)
|
||||
|
|
|
@ -265,12 +265,6 @@ void addBuildDockerContextTask(Architecture architecture, DockerBase base) {
|
|||
}
|
||||
// For some reason, the artifact name can differ depending on what repository we used.
|
||||
rename ~/((?:file|metric)beat)-.*\.tar\.gz$/, "\$1-${VersionProperties.elasticsearch}.tar.gz"
|
||||
|
||||
into('bin') {
|
||||
from(project.projectDir.toPath().resolve('src/docker/cloud')) {
|
||||
expand([ version: VersionProperties.elasticsearch ])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onlyIf { Architecture.current() == architecture }
|
||||
|
|
|
@ -110,12 +110,17 @@ RUN sed -i -e 's/ES_DISTRIBUTION_TYPE=tar/ES_DISTRIBUTION_TYPE=docker/' bin/elas
|
|||
find config -type f -exec chmod 0664 {} +
|
||||
|
||||
<% if (docker_base == "cloud") { %>
|
||||
# Preinstall common plugins
|
||||
# Preinstall common plugins. Note that these are installed as root, meaning the `elasticsearch` user cannot delete them.
|
||||
COPY repository-s3-${version}.zip repository-gcs-${version}.zip repository-azure-${version}.zip /tmp/
|
||||
RUN bin/elasticsearch-plugin install --batch \\
|
||||
RUN bin/elasticsearch-plugin install --batch --verbose \\
|
||||
file:/tmp/repository-s3-${version}.zip \\
|
||||
file:/tmp/repository-gcs-${version}.zip \\
|
||||
file:/tmp/repository-azure-${version}.zip
|
||||
# Generate a replacement example plugins config that reflects what is actually installed
|
||||
RUN echo "plugins:" > config/elasticsearch-plugins.example.yml && \\
|
||||
echo " - id: repository-azure" >> config/elasticsearch-plugins.example.yml && \\
|
||||
echo " - id: repository-gcs" >> config/elasticsearch-plugins.example.yml && \\
|
||||
echo " - id: repository-s3" >> config/elasticsearch-plugins.example.yml
|
||||
|
||||
<% /* I tried to use `ADD` here, but I couldn't force it to do what I wanted */ %>
|
||||
COPY filebeat-${version}.tar.gz metricbeat-${version}.tar.gz /tmp/
|
||||
|
@ -125,8 +130,6 @@ RUN mkdir -p /opt/filebeat /opt/metricbeat && \\
|
|||
|
||||
# Add plugins infrastructure
|
||||
RUN mkdir -p /opt/plugins/archive
|
||||
COPY bin/plugin-wrapper.sh /opt/plugins
|
||||
# These are the correct permissions for both the directories and the script
|
||||
RUN chmod -R 0555 /opt/plugins
|
||||
<% } %>
|
||||
|
||||
|
|
|
@ -10,3 +10,4 @@ RUN chmod 0444 /opt/plugins/archive/*
|
|||
FROM ${base_image}
|
||||
|
||||
COPY --from=builder /opt/plugins /opt/plugins
|
||||
ENV ES_PLUGIN_ARCHIVE_DIR /opt/plugins/archive
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
# or more contributor license agreements. Licensed under the Elastic License
|
||||
# 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
# in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
# Side Public License, v 1.
|
||||
#
|
||||
|
||||
<% /* Populated by Gradle */ %>
|
||||
VERSION="$version"
|
||||
|
||||
plugin_name_is_next=0
|
||||
|
||||
declare -a args_array
|
||||
|
||||
while test \$# -gt 0; do
|
||||
opt="\$1"
|
||||
shift
|
||||
|
||||
if [[ \$plugin_name_is_next -eq 1 ]]; then
|
||||
if [[ -f "/opt/plugins/archive/\$opt-\${VERSION}.zip" ]]; then
|
||||
opt="file:/opt/plugins/archive/\$opt-\${VERSION}.zip"
|
||||
fi
|
||||
elif [[ "\$opt" == "install" ]]; then
|
||||
plugin_name_is_next=1
|
||||
fi
|
||||
|
||||
args_array+=("\$opt")
|
||||
done
|
||||
|
||||
set -- "\$@" "\${args_array[@]}"
|
||||
|
||||
exec /usr/share/elasticsearch/bin/elasticsearch-plugin "\$@"
|
|
@ -175,6 +175,7 @@ def commonPackageConfig(String type, String architecture) {
|
|||
|
||||
// ========= config files =========
|
||||
configurationFile '/etc/elasticsearch/elasticsearch.yml'
|
||||
configurationFile '/etc/elasticsearch/elasticsearch-plugins.example.yml'
|
||||
configurationFile '/etc/elasticsearch/jvm.options'
|
||||
configurationFile '/etc/elasticsearch/log4j2.properties'
|
||||
configurationFile '/etc/elasticsearch/role_mapping.yml'
|
||||
|
|
27
distribution/src/config/elasticsearch-plugins.example.yml
Normal file
27
distribution/src/config/elasticsearch-plugins.example.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
# Rename this file to `elasticsearch-plugins.yml` to use it.
|
||||
#
|
||||
# All plugins must be listed here. If you add a plugin to this list and run
|
||||
# `elasticsearch-plugin sync`, that plugin will be installed. If you remove
|
||||
# a plugin from this list, that plugin will be removed when Elasticsearch
|
||||
# next starts.
|
||||
|
||||
plugins:
|
||||
# Each plugin must have an ID. Plugins with only an ID are official plugins and will be downloaded from Elastic.
|
||||
- id: example-id
|
||||
|
||||
# Plugins can be specified by URL (it doesn't have to be HTTP, you could use e.g. `file:`)
|
||||
- id: example-with-url
|
||||
location: https://some.domain/path/example4.zip
|
||||
|
||||
# Or by maven coordinates:
|
||||
- id: example-with-maven-url
|
||||
location: org.elasticsearch.plugins:example-plugin:1.2.3
|
||||
|
||||
# A proxy can also be configured per-plugin, if necessary
|
||||
- id: example-with-proxy
|
||||
location: https://some.domain/path/example.zip
|
||||
proxy: https://some.domain:1234
|
||||
|
||||
# Configures a proxy for all network access. Remove this if you don't need
|
||||
# to use a proxy.
|
||||
proxy: https://some.domain:1234
|
|
@ -13,10 +13,6 @@ archivesBaseName = 'elasticsearch-plugin-cli'
|
|||
dependencies {
|
||||
compileOnly project(":server")
|
||||
compileOnly project(":libs:elasticsearch-cli")
|
||||
api "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}"
|
||||
api "com.fasterxml.jackson.core:jackson-core:${versions.jackson}"
|
||||
api "com.fasterxml.jackson.core:jackson-databind:${versions.jackson}"
|
||||
api "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${versions.jackson}"
|
||||
api "org.bouncycastle:bcpg-fips:1.0.4"
|
||||
api "org.bouncycastle:bc-fips:1.0.2"
|
||||
testImplementation project(":test:framework")
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
6ae6028aff033f194c9710ad87c224ccaadeed6c
|
|
@ -1,8 +0,0 @@
|
|||
This copy of Jackson JSON processor annotations is licensed under the
|
||||
Apache (Software) License, version 2.0 ("the License").
|
||||
See the License for details about distribution rights, and the
|
||||
specific rights regarding derivate works.
|
||||
|
||||
You may obtain a copy of the License at:
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
@ -1,20 +0,0 @@
|
|||
# Jackson JSON processor
|
||||
|
||||
Jackson is a high-performance, Free/Open Source JSON processing library.
|
||||
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
|
||||
been in development since 2007.
|
||||
It is currently developed by a community of developers, as well as supported
|
||||
commercially by FasterXML.com.
|
||||
|
||||
## Licensing
|
||||
|
||||
Jackson core and extension components may be licensed under different licenses.
|
||||
To find the details that apply to this artifact see the accompanying LICENSE file.
|
||||
For more information, including possible other licensing options, contact
|
||||
FasterXML.com (http://fasterxml.com).
|
||||
|
||||
## Credits
|
||||
|
||||
A list of contributors may be found from CREDITS file, which is included
|
||||
in some artifacts (usually source distributions); but is always available
|
||||
from the source code management (SCM) system project uses.
|
|
@ -1 +0,0 @@
|
|||
76e9152e93d4cf052f93a64596f633ba5b1c8ed9
|
|
@ -1,8 +0,0 @@
|
|||
This copy of Jackson JSON processor databind module is licensed under the
|
||||
Apache (Software) License, version 2.0 ("the License").
|
||||
See the License for details about distribution rights, and the
|
||||
specific rights regarding derivate works.
|
||||
|
||||
You may obtain a copy of the License at:
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
@ -1,20 +0,0 @@
|
|||
# Jackson JSON processor
|
||||
|
||||
Jackson is a high-performance, Free/Open Source JSON processing library.
|
||||
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
|
||||
been in development since 2007.
|
||||
It is currently developed by a community of developers, as well as supported
|
||||
commercially by FasterXML.com.
|
||||
|
||||
## Licensing
|
||||
|
||||
Jackson core and extension components may be licensed under different licenses.
|
||||
To find the details that apply to this artifact see the accompanying LICENSE file.
|
||||
For more information, including possible other licensing options, contact
|
||||
FasterXML.com (http://fasterxml.com).
|
||||
|
||||
## Credits
|
||||
|
||||
A list of contributors may be found from CREDITS file, which is included
|
||||
in some artifacts (usually source distributions); but is always available
|
||||
from the source code management (SCM) system project uses.
|
|
@ -32,6 +32,7 @@ import org.elasticsearch.cli.UserException;
|
|||
import org.elasticsearch.common.hash.MessageDigests;
|
||||
import org.elasticsearch.common.io.Streams;
|
||||
import org.elasticsearch.common.util.set.Sets;
|
||||
import org.elasticsearch.core.PathUtils;
|
||||
import org.elasticsearch.core.SuppressForbidden;
|
||||
import org.elasticsearch.core.Tuple;
|
||||
import org.elasticsearch.core.internal.io.IOUtils;
|
||||
|
@ -49,6 +50,7 @@ import java.io.InputStreamReader;
|
|||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Proxy;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
|
@ -114,7 +116,7 @@ import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE;
|
|||
* elasticsearch config directory, using the name of the plugin. If any files to be installed
|
||||
* already exist, they will be skipped.
|
||||
*/
|
||||
class InstallPluginAction implements Closeable {
|
||||
public class InstallPluginAction implements Closeable {
|
||||
|
||||
private static final String PROPERTY_STAGING_ID = "es.plugins.staging";
|
||||
|
||||
|
@ -142,7 +144,7 @@ class InstallPluginAction implements Closeable {
|
|||
}
|
||||
|
||||
/** The official plugins that can be installed simply by name. */
|
||||
static final Set<String> OFFICIAL_PLUGINS;
|
||||
public static final Set<String> OFFICIAL_PLUGINS;
|
||||
static {
|
||||
try (var stream = InstallPluginAction.class.getResourceAsStream("/plugins.txt")) {
|
||||
OFFICIAL_PLUGINS = Streams.readAllLines(stream).stream().map(String::trim).collect(Sets.toUnmodifiableSortedSet());
|
||||
|
@ -181,15 +183,20 @@ class InstallPluginAction implements Closeable {
|
|||
private final Terminal terminal;
|
||||
private Environment env;
|
||||
private boolean batch;
|
||||
private Proxy proxy = null;
|
||||
|
||||
InstallPluginAction(Terminal terminal, Environment env, boolean batch) {
|
||||
public InstallPluginAction(Terminal terminal, Environment env, boolean batch) {
|
||||
this.terminal = terminal;
|
||||
this.env = env;
|
||||
this.batch = batch;
|
||||
}
|
||||
|
||||
public void setProxy(Proxy proxy) {
|
||||
this.proxy = proxy;
|
||||
}
|
||||
|
||||
// pkg private for testing
|
||||
void execute(List<PluginDescriptor> plugins) throws Exception {
|
||||
public void execute(List<PluginDescriptor> plugins) throws Exception {
|
||||
if (plugins.isEmpty()) {
|
||||
throw new UserException(ExitCodes.USAGE, "at least one plugin id is required");
|
||||
}
|
||||
|
@ -201,10 +208,12 @@ class InstallPluginAction implements Closeable {
|
|||
}
|
||||
}
|
||||
|
||||
final String logPrefix = terminal.isHeadless() ? "" : "-> ";
|
||||
|
||||
final Map<String, List<Path>> deleteOnFailures = new LinkedHashMap<>();
|
||||
for (final PluginDescriptor plugin : plugins) {
|
||||
final String pluginId = plugin.getId();
|
||||
terminal.println("-> Installing " + pluginId);
|
||||
terminal.println(logPrefix + "Installing " + pluginId);
|
||||
try {
|
||||
if ("x-pack".equals(pluginId)) {
|
||||
handleInstallXPack(buildFlavor());
|
||||
|
@ -216,15 +225,15 @@ class InstallPluginAction implements Closeable {
|
|||
final Path pluginZip = download(plugin, env.tmpFile());
|
||||
final Path extractedZip = unzip(pluginZip, env.pluginsFile());
|
||||
deleteOnFailure.add(extractedZip);
|
||||
final PluginInfo pluginInfo = installPlugin(extractedZip, deleteOnFailure);
|
||||
terminal.println("-> Installed " + pluginInfo.getName());
|
||||
final PluginInfo pluginInfo = installPlugin(plugin, extractedZip, deleteOnFailure);
|
||||
terminal.println(logPrefix + "Installed " + pluginInfo.getName());
|
||||
// swap the entry by plugin id for one with the installed plugin name, it gives a cleaner error message for URL installs
|
||||
deleteOnFailures.remove(pluginId);
|
||||
deleteOnFailures.put(pluginInfo.getName(), deleteOnFailure);
|
||||
} catch (final Exception installProblem) {
|
||||
terminal.println("-> Failed installing " + pluginId);
|
||||
terminal.println(logPrefix + "Failed installing " + pluginId);
|
||||
for (final Map.Entry<String, List<Path>> deleteOnFailureEntry : deleteOnFailures.entrySet()) {
|
||||
terminal.println("-> Rolling back " + deleteOnFailureEntry.getKey());
|
||||
terminal.println(logPrefix + "Rolling back " + deleteOnFailureEntry.getKey());
|
||||
boolean success = false;
|
||||
try {
|
||||
IOUtils.rm(deleteOnFailureEntry.getValue().toArray(new Path[0]));
|
||||
|
@ -235,16 +244,18 @@ class InstallPluginAction implements Closeable {
|
|||
exceptionWhileRemovingFiles
|
||||
);
|
||||
installProblem.addSuppressed(exception);
|
||||
terminal.println("-> Failed rolling back " + deleteOnFailureEntry.getKey());
|
||||
terminal.println(logPrefix + "Failed rolling back " + deleteOnFailureEntry.getKey());
|
||||
}
|
||||
if (success) {
|
||||
terminal.println("-> Rolled back " + deleteOnFailureEntry.getKey());
|
||||
terminal.println(logPrefix + "Rolled back " + deleteOnFailureEntry.getKey());
|
||||
}
|
||||
}
|
||||
throw installProblem;
|
||||
}
|
||||
}
|
||||
terminal.println("-> Please restart Elasticsearch to activate any plugins installed");
|
||||
if (terminal.isHeadless() == false) {
|
||||
terminal.println("-> Please restart Elasticsearch to activate any plugins installed");
|
||||
}
|
||||
}
|
||||
|
||||
Build.Flavor buildFlavor() {
|
||||
|
@ -271,24 +282,37 @@ class InstallPluginAction implements Closeable {
|
|||
private Path download(PluginDescriptor plugin, Path tmpDir) throws Exception {
|
||||
final String pluginId = plugin.getId();
|
||||
|
||||
if (OFFICIAL_PLUGINS.contains(pluginId)) {
|
||||
final String logPrefix = terminal.isHeadless() ? "" : "-> ";
|
||||
|
||||
// See `InstallPluginCommand` it has to use a string argument for both the ID and the location
|
||||
if (OFFICIAL_PLUGINS.contains(pluginId) && (plugin.getLocation() == null || plugin.getLocation().equals(pluginId))) {
|
||||
final String pluginArchiveDir = System.getenv("ES_PLUGIN_ARCHIVE_DIR");
|
||||
if (pluginArchiveDir != null && pluginArchiveDir.isEmpty() == false) {
|
||||
final Path pluginPath = getPluginArchivePath(pluginId, pluginArchiveDir);
|
||||
if (Files.exists(pluginPath)) {
|
||||
terminal.println(logPrefix + "Downloading " + pluginId + " from local archive: " + pluginArchiveDir);
|
||||
return downloadZip("file://" + pluginPath, tmpDir);
|
||||
}
|
||||
// else carry on to regular download
|
||||
}
|
||||
|
||||
final String url = getElasticUrl(getStagingHash(), Version.CURRENT, isSnapshot(), pluginId, Platforms.PLATFORM_NAME);
|
||||
terminal.println("-> Downloading " + pluginId + " from elastic");
|
||||
terminal.println(logPrefix + "Downloading " + pluginId + " from elastic");
|
||||
return downloadAndValidate(url, tmpDir, true);
|
||||
}
|
||||
|
||||
final String pluginUrl = plugin.getUrl();
|
||||
final String pluginLocation = plugin.getLocation();
|
||||
|
||||
// now try as maven coordinates, a valid URL would only have a colon and slash
|
||||
String[] coordinates = pluginUrl.split(":");
|
||||
if (coordinates.length == 3 && pluginUrl.contains("/") == false && pluginUrl.startsWith("file:") == false) {
|
||||
String mavenUrl = getMavenUrl(coordinates, Platforms.PLATFORM_NAME);
|
||||
terminal.println("-> Downloading " + pluginId + " from maven central");
|
||||
String[] coordinates = pluginLocation.split(":");
|
||||
if (coordinates.length == 3 && pluginLocation.contains("/") == false && pluginLocation.startsWith("file:") == false) {
|
||||
String mavenUrl = getMavenUrl(coordinates);
|
||||
terminal.println(logPrefix + "Downloading " + pluginId + " from maven central");
|
||||
return downloadAndValidate(mavenUrl, tmpDir, false);
|
||||
}
|
||||
|
||||
// fall back to plain old URL
|
||||
if (pluginUrl.contains(":") == false) {
|
||||
if (pluginLocation.contains(":") == false) {
|
||||
// definitely not a valid url, so assume it is a plugin name
|
||||
List<String> pluginSuggestions = checkMisspelledPlugin(pluginId);
|
||||
String msg = "Unknown plugin " + pluginId;
|
||||
|
@ -297,8 +321,20 @@ class InstallPluginAction implements Closeable {
|
|||
}
|
||||
throw new UserException(ExitCodes.USAGE, msg);
|
||||
}
|
||||
terminal.println("-> Downloading " + URLDecoder.decode(pluginUrl, StandardCharsets.UTF_8));
|
||||
return downloadZip(pluginUrl, tmpDir);
|
||||
terminal.println(logPrefix + "Downloading " + URLDecoder.decode(pluginLocation, StandardCharsets.UTF_8));
|
||||
return downloadZip(pluginLocation, tmpDir);
|
||||
}
|
||||
|
||||
@SuppressForbidden(reason = "Need to use PathUtils#get")
|
||||
private Path getPluginArchivePath(String pluginId, String pluginArchiveDir) throws UserException {
|
||||
final Path path = PathUtils.get(pluginArchiveDir);
|
||||
if (Files.exists(path) == false) {
|
||||
throw new UserException(ExitCodes.CONFIG, "Location in ES_PLUGIN_ARCHIVE_DIR does not exist");
|
||||
}
|
||||
if (Files.isDirectory(path) == false) {
|
||||
throw new UserException(ExitCodes.CONFIG, "Location in ES_PLUGIN_ARCHIVE_DIR is not a directory");
|
||||
}
|
||||
return PathUtils.get(pluginArchiveDir, pluginId + "-" + Version.CURRENT + (isSnapshot() ? "-SNAPSHOT" : "") + ".zip");
|
||||
}
|
||||
|
||||
// pkg private so tests can override
|
||||
|
@ -364,12 +400,12 @@ class InstallPluginAction implements Closeable {
|
|||
/**
|
||||
* Returns the url for an elasticsearch plugin in maven.
|
||||
*/
|
||||
private String getMavenUrl(String[] coordinates, String platform) throws IOException {
|
||||
private String getMavenUrl(String[] coordinates) throws IOException {
|
||||
final String groupId = coordinates[0].replace(".", "/");
|
||||
final String artifactId = coordinates[1];
|
||||
final String version = coordinates[2];
|
||||
final String baseUrl = String.format(Locale.ROOT, "https://repo1.maven.org/maven2/%s/%s/%s", groupId, artifactId, version);
|
||||
final String platformUrl = String.format(Locale.ROOT, "%s/%s-%s-%s.zip", baseUrl, artifactId, platform, version);
|
||||
final String platformUrl = String.format(Locale.ROOT, "%s/%s-%s-%s.zip", baseUrl, artifactId, Platforms.PLATFORM_NAME, version);
|
||||
if (urlExists(platformUrl)) {
|
||||
return platformUrl;
|
||||
}
|
||||
|
@ -407,7 +443,7 @@ class InstallPluginAction implements Closeable {
|
|||
}
|
||||
}
|
||||
CollectionUtil.timSort(scoredKeys, (a, b) -> b.v1().compareTo(a.v1()));
|
||||
return scoredKeys.stream().map((a) -> a.v2()).collect(Collectors.toList());
|
||||
return scoredKeys.stream().map(Tuple::v2).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/** Downloads a zip from the url, into a temp file under the given temp dir. */
|
||||
|
@ -417,10 +453,10 @@ class InstallPluginAction implements Closeable {
|
|||
terminal.println(VERBOSE, "Retrieving zip from " + urlString);
|
||||
URL url = new URL(urlString);
|
||||
Path zip = Files.createTempFile(tmpDir, null, ".zip");
|
||||
URLConnection urlConnection = url.openConnection();
|
||||
URLConnection urlConnection = this.proxy == null ? url.openConnection() : url.openConnection(this.proxy);
|
||||
urlConnection.addRequestProperty("User-Agent", "elasticsearch-plugin-installer");
|
||||
try (
|
||||
InputStream in = batch
|
||||
InputStream in = batch || terminal.isHeadless()
|
||||
? urlConnection.getInputStream()
|
||||
: new TerminalProgressInputStream(urlConnection.getInputStream(), urlConnection.getContentLength(), terminal)
|
||||
) {
|
||||
|
@ -443,10 +479,10 @@ class InstallPluginAction implements Closeable {
|
|||
/**
|
||||
* content length might be -1 for unknown and progress only makes sense if the content length is greater than 0
|
||||
*/
|
||||
private class TerminalProgressInputStream extends ProgressInputStream {
|
||||
private static class TerminalProgressInputStream extends ProgressInputStream {
|
||||
private static final int WIDTH = 50;
|
||||
|
||||
private final Terminal terminal;
|
||||
private int width = 50;
|
||||
private final boolean enabled;
|
||||
|
||||
TerminalProgressInputStream(InputStream is, int expectedTotalSize, Terminal terminal) {
|
||||
|
@ -458,13 +494,13 @@ class InstallPluginAction implements Closeable {
|
|||
@Override
|
||||
public void onProgress(int percent) {
|
||||
if (enabled) {
|
||||
int currentPosition = percent * width / 100;
|
||||
int currentPosition = percent * WIDTH / 100;
|
||||
StringBuilder sb = new StringBuilder("\r[");
|
||||
sb.append(String.join("=", Collections.nCopies(currentPosition, "")));
|
||||
if (currentPosition > 0 && percent < 100) {
|
||||
sb.append(">");
|
||||
}
|
||||
sb.append(String.join(" ", Collections.nCopies(width - currentPosition, "")));
|
||||
sb.append(String.join(" ", Collections.nCopies(WIDTH - currentPosition, "")));
|
||||
sb.append("] %s ");
|
||||
if (percent == 100) {
|
||||
sb.append("\n");
|
||||
|
@ -476,7 +512,7 @@ class InstallPluginAction implements Closeable {
|
|||
|
||||
@SuppressForbidden(reason = "URL#openStream")
|
||||
private InputStream urlOpenStream(final URL url) throws IOException {
|
||||
return url.openStream();
|
||||
return this.proxy == null ? url.openStream() : url.openConnection(proxy).getInputStream();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -528,14 +564,10 @@ class InstallPluginAction implements Closeable {
|
|||
* matches, and that the file contains a single line. For SHA-512, we verify that the hash and the filename match, and that the
|
||||
* file contains a single line.
|
||||
*/
|
||||
final BufferedReader checksumReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
if (digestAlgo.equals("SHA-1")) {
|
||||
final BufferedReader checksumReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
expectedChecksum = checksumReader.readLine();
|
||||
if (checksumReader.readLine() != null) {
|
||||
throw new UserException(ExitCodes.IO_ERROR, "Invalid checksum file at " + checksumUrl);
|
||||
}
|
||||
} else {
|
||||
final BufferedReader checksumReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
final String checksumLine = checksumReader.readLine();
|
||||
final String[] fields = checksumLine.split(" {2}");
|
||||
if (officialPlugin && fields.length != 2 || officialPlugin == false && fields.length > 2) {
|
||||
|
@ -557,9 +589,9 @@ class InstallPluginAction implements Closeable {
|
|||
throw new UserException(ExitCodes.IO_ERROR, message);
|
||||
}
|
||||
}
|
||||
if (checksumReader.readLine() != null) {
|
||||
throw new UserException(ExitCodes.IO_ERROR, "Invalid checksum file at " + checksumUrl);
|
||||
}
|
||||
}
|
||||
if (checksumReader.readLine() != null) {
|
||||
throw new UserException(ExitCodes.IO_ERROR, "Invalid checksum file at " + checksumUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -598,7 +630,7 @@ class InstallPluginAction implements Closeable {
|
|||
* ".asc" to the URL. It is expected that the plugin is signed with the Elastic signing key with ID D27D666CD88E42B4.
|
||||
*
|
||||
* @param zip the path to the downloaded plugin ZIP
|
||||
* @param urlString the URL source of the downloade plugin ZIP
|
||||
* @param urlString the URL source of the downloaded plugin ZIP
|
||||
* @throws IOException if an I/O exception occurs reading from various input streams
|
||||
* @throws PGPException if the PGP implementation throws an internal exception during verification
|
||||
*/
|
||||
|
@ -671,12 +703,14 @@ class InstallPluginAction implements Closeable {
|
|||
/**
|
||||
* Creates a URL and opens a connection.
|
||||
* <p>
|
||||
* If the URL returns a 404, {@code null} is returned, otherwise the open URL opject is returned.
|
||||
* If the URL returns a 404, {@code null} is returned, otherwise the open URL object is returned.
|
||||
*/
|
||||
// pkg private for tests
|
||||
URL openUrl(String urlString) throws IOException {
|
||||
URL checksumUrl = new URL(urlString);
|
||||
HttpURLConnection connection = (HttpURLConnection) checksumUrl.openConnection();
|
||||
HttpURLConnection connection = this.proxy == null
|
||||
? (HttpURLConnection) checksumUrl.openConnection()
|
||||
: (HttpURLConnection) checksumUrl.openConnection(this.proxy);
|
||||
if (connection.getResponseCode() == 404) {
|
||||
return null;
|
||||
}
|
||||
|
@ -832,7 +866,7 @@ class InstallPluginAction implements Closeable {
|
|||
* Installs the plugin from {@code tmpRoot} into the plugins dir.
|
||||
* If the plugin has a bin dir and/or a config dir, those are moved.
|
||||
*/
|
||||
private PluginInfo installPlugin(Path tmpRoot, List<Path> deleteOnFailure) throws Exception {
|
||||
private PluginInfo installPlugin(PluginDescriptor descriptor, Path tmpRoot, List<Path> deleteOnFailure) throws Exception {
|
||||
final PluginInfo info = loadPluginInfo(tmpRoot);
|
||||
checkCanInstallationProceed(terminal, Build.CURRENT.flavor(), info);
|
||||
PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(tmpRoot, env.tmpFile());
|
||||
|
@ -841,6 +875,16 @@ class InstallPluginAction implements Closeable {
|
|||
PluginSecurity.confirmPolicyExceptions(terminal, permissions, batch);
|
||||
}
|
||||
|
||||
// Validate that the downloaded plugin's ID matches what we expect from the descriptor. The
|
||||
// exception is if we install a plugin via `InstallPluginCommand` by specifying a URL or
|
||||
// Maven coordinates, because then we can't know in advance what the plugin ID ought to be.
|
||||
if (descriptor.getId().contains(":") == false && descriptor.getId().equals(info.getName()) == false) {
|
||||
throw new UserException(
|
||||
PLUGIN_MALFORMED,
|
||||
"Expected downloaded plugin to have ID [" + descriptor.getId() + "] but found [" + info.getName() + "]"
|
||||
);
|
||||
}
|
||||
|
||||
final Path destination = env.pluginsFile().resolve(info.getName());
|
||||
deleteOnFailure.add(destination);
|
||||
|
||||
|
@ -876,10 +920,10 @@ class InstallPluginAction implements Closeable {
|
|||
|
||||
/**
|
||||
* Moves the plugin directory into its final destination.
|
||||
**/
|
||||
*/
|
||||
private void movePlugin(Path tmpRoot, Path destination) throws IOException {
|
||||
Files.move(tmpRoot, destination, StandardCopyOption.ATOMIC_MOVE);
|
||||
Files.walkFileTree(destination, new SimpleFileVisitor<Path>() {
|
||||
Files.walkFileTree(destination, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
|
||||
final String parentDirName = file.getParent().getFileName().toString();
|
||||
|
@ -991,10 +1035,10 @@ class InstallPluginAction implements Closeable {
|
|||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
IOUtils.rm(pathsToDeleteOnShutdown.toArray(new Path[pathsToDeleteOnShutdown.size()]));
|
||||
IOUtils.rm(pathsToDeleteOnShutdown.toArray(new Path[0]));
|
||||
}
|
||||
|
||||
static void checkCanInstallationProceed(Terminal terminal, Build.Flavor flavor, PluginInfo info) throws Exception {
|
||||
public static void checkCanInstallationProceed(Terminal terminal, Build.Flavor flavor, PluginInfo info) throws Exception {
|
||||
if (info.isLicensed() == false) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -75,13 +75,17 @@ class InstallPluginCommand extends EnvironmentAwareCommand {
|
|||
|
||||
@Override
|
||||
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
||||
SyncPluginsAction.ensureNoConfigFile(env);
|
||||
|
||||
List<PluginDescriptor> plugins = arguments.values(options)
|
||||
.stream()
|
||||
.map(id -> new PluginDescriptor(id, id))
|
||||
// We only have one piece of data, which could be an ID or could be a location, so we use it for both
|
||||
.map(idOrLocation -> new PluginDescriptor(idOrLocation, idOrLocation))
|
||||
.collect(Collectors.toList());
|
||||
final boolean isBatch = options.has(batchOption);
|
||||
|
||||
InstallPluginAction action = new InstallPluginAction(terminal, env, isBatch);
|
||||
action.execute(plugins);
|
||||
try (InstallPluginAction action = new InstallPluginAction(terminal, env, isBatch)) {
|
||||
action.execute(plugins);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,8 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.elasticsearch.plugins.cli.SyncPluginsAction.ELASTICSEARCH_PLUGINS_YML_CACHE;
|
||||
|
||||
/**
|
||||
* A command for the plugin cli to list plugins installed in elasticsearch.
|
||||
*/
|
||||
|
@ -42,8 +44,10 @@ class ListPluginsCommand extends EnvironmentAwareCommand {
|
|||
terminal.println(Terminal.Verbosity.VERBOSE, "Plugins directory: " + env.pluginsFile());
|
||||
final List<Path> plugins = new ArrayList<>();
|
||||
try (DirectoryStream<Path> paths = Files.newDirectoryStream(env.pluginsFile())) {
|
||||
for (Path plugin : paths) {
|
||||
plugins.add(plugin);
|
||||
for (Path path : paths) {
|
||||
if (path.getFileName().toString().equals(ELASTICSEARCH_PLUGINS_YML_CACHE) == false) {
|
||||
plugins.add(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Collections.sort(plugins);
|
||||
|
|
|
@ -10,25 +10,29 @@ package org.elasticsearch.plugins.cli;
|
|||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Models a single plugin that can be installed.
|
||||
*/
|
||||
public class PluginDescriptor {
|
||||
private String id;
|
||||
private String url;
|
||||
private String proxy;
|
||||
private String location;
|
||||
|
||||
public PluginDescriptor() {}
|
||||
|
||||
public PluginDescriptor(String id, String url, String proxy) {
|
||||
this.id = id;
|
||||
this.url = url;
|
||||
this.proxy = proxy;
|
||||
}
|
||||
|
||||
public PluginDescriptor(String id, String url) {
|
||||
this(id, url, null);
|
||||
/**
|
||||
* Creates a new descriptor instance.
|
||||
*
|
||||
* @param id the name of the plugin. Cannot be null.
|
||||
* @param location the location from which to fetch the plugin, e.g. a URL or Maven
|
||||
* coordinates. Can be null for official plugins.
|
||||
*/
|
||||
public PluginDescriptor(String id, String location) {
|
||||
this.id = Objects.requireNonNull(id, "id cannot be null");
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public PluginDescriptor(String id) {
|
||||
this(id, null, null);
|
||||
this(id, null);
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
|
@ -39,20 +43,12 @@ public class PluginDescriptor {
|
|||
this.id = id;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getProxy() {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
public void setProxy(String proxy) {
|
||||
this.proxy = proxy;
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -60,11 +56,16 @@ public class PluginDescriptor {
|
|||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
PluginDescriptor that = (PluginDescriptor) o;
|
||||
return id.equals(that.id) && Objects.equals(url, that.url) && Objects.equals(proxy, that.proxy);
|
||||
return id.equals(that.id) && Objects.equals(location, that.location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id, url, proxy);
|
||||
return Objects.hash(id, location);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PluginDescriptor{id='" + id + "', location='" + location + "'}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,10 @@ import java.util.List;
|
|||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Contains methods for displaying extended plugin permissions to the user, and confirming that
|
||||
* plugin installation can proceed.
|
||||
*/
|
||||
public class PluginSecurity {
|
||||
|
||||
/**
|
||||
|
@ -37,30 +41,45 @@ public class PluginSecurity {
|
|||
if (requested.isEmpty()) {
|
||||
terminal.println(Verbosity.VERBOSE, "plugin has a policy file with no additional permissions");
|
||||
} else {
|
||||
|
||||
// sort permissions in a reasonable order
|
||||
Collections.sort(requested);
|
||||
|
||||
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
|
||||
terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @");
|
||||
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
|
||||
// print all permissions:
|
||||
for (String permission : requested) {
|
||||
terminal.errorPrintln(Verbosity.NORMAL, "* " + permission);
|
||||
if (terminal.isHeadless()) {
|
||||
terminal.errorPrintln(
|
||||
"WARNING: plugin requires additional permissions: ["
|
||||
+ requested.stream().map(each -> '\'' + each + '\'').collect(Collectors.joining(", "))
|
||||
+ "]"
|
||||
);
|
||||
terminal.errorPrintln(
|
||||
"See https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"
|
||||
+ " for descriptions of what these permissions allow and the associated risks."
|
||||
);
|
||||
} else {
|
||||
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
|
||||
terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @");
|
||||
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
|
||||
// print all permissions:
|
||||
for (String permission : requested) {
|
||||
terminal.errorPrintln(Verbosity.NORMAL, "* " + permission);
|
||||
}
|
||||
terminal.errorPrintln(
|
||||
Verbosity.NORMAL,
|
||||
"See https://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html"
|
||||
);
|
||||
terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks.");
|
||||
|
||||
if (batch == false) {
|
||||
prompt(terminal);
|
||||
}
|
||||
}
|
||||
terminal.errorPrintln(Verbosity.NORMAL, "See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html");
|
||||
terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks.");
|
||||
prompt(terminal, batch);
|
||||
}
|
||||
}
|
||||
|
||||
private static void prompt(final Terminal terminal, final boolean batch) throws UserException {
|
||||
if (batch == false) {
|
||||
terminal.println(Verbosity.NORMAL, "");
|
||||
String text = terminal.readText("Continue with installation? [y/N]");
|
||||
if (text.equalsIgnoreCase("y") == false) {
|
||||
throw new UserException(ExitCodes.DATA_ERROR, "installation aborted by user");
|
||||
}
|
||||
private static void prompt(final Terminal terminal) throws UserException {
|
||||
terminal.println(Verbosity.NORMAL, "");
|
||||
String text = terminal.readText("Continue with installation? [y/N]");
|
||||
if (text.equalsIgnoreCase("y") == false) {
|
||||
throw new UserException(ExitCodes.DATA_ERROR, "installation aborted by user");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,7 +122,7 @@ public class PluginSecurity {
|
|||
/**
|
||||
* Extract a unique set of permissions from the plugin's policy file. Each permission is formatted for output to users.
|
||||
*/
|
||||
static Set<String> getPermissionDescriptions(PluginPolicyInfo pluginPolicyInfo, Path tmpDir) throws IOException {
|
||||
public static Set<String> getPermissionDescriptions(PluginPolicyInfo pluginPolicyInfo, Path tmpDir) throws IOException {
|
||||
Set<Permission> allPermissions = new HashSet<>(PolicyUtil.getPolicyPermissions(null, pluginPolicyInfo.policy, tmpDir));
|
||||
for (URL jar : pluginPolicyInfo.jars) {
|
||||
Set<Permission> jarPermissions = PolicyUtil.getPolicyPermissions(jar, pluginPolicyInfo.policy, tmpDir);
|
||||
|
|
|
@ -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
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.plugins.cli;
|
||||
|
||||
/**
|
||||
* Thrown when a problem occurs synchronising plugins.
|
||||
*/
|
||||
class PluginSyncException extends Exception {
|
||||
|
||||
PluginSyncException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
PluginSyncException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.plugins.cli;
|
||||
|
||||
import org.elasticsearch.xcontent.ObjectParser;
|
||||
import org.elasticsearch.xcontent.ParseField;
|
||||
import org.elasticsearch.xcontent.XContent;
|
||||
import org.elasticsearch.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.xcontent.XContentParser;
|
||||
import org.elasticsearch.xcontent.XContentParserConfiguration;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* This class models the contents of the {@code elasticsearch-plugins.yml} file. This file specifies all the plugins
|
||||
* that ought to be installed in an Elasticsearch instance, and where to find them if they are not an official
|
||||
* Elasticsearch plugin.
|
||||
*/
|
||||
public class PluginsConfig {
|
||||
private List<PluginDescriptor> plugins;
|
||||
private String proxy;
|
||||
|
||||
public PluginsConfig() {
|
||||
plugins = List.of();
|
||||
proxy = null;
|
||||
}
|
||||
|
||||
public void setPlugins(List<PluginDescriptor> plugins) {
|
||||
this.plugins = plugins == null ? List.of() : plugins;
|
||||
}
|
||||
|
||||
public void setProxy(String proxy) {
|
||||
this.proxy = proxy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate this instance. For example:
|
||||
* <ul>
|
||||
* <li>All {@link PluginDescriptor}s must have IDs</li>
|
||||
* <li>Any proxy must be well-formed.</li>
|
||||
* <li>Unofficial plugins must have locations</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param officialPlugins the plugins that can be installed by name only
|
||||
* @throws PluginSyncException if validation problems are found
|
||||
*/
|
||||
public void validate(Set<String> officialPlugins) throws PluginSyncException {
|
||||
if (this.plugins.stream().anyMatch(each -> each == null || each.getId() == null || each.getId().isBlank())) {
|
||||
throw new RuntimeException("Cannot have null or empty IDs in [elasticsearch-plugins.yml]");
|
||||
}
|
||||
|
||||
final Set<String> uniquePluginIds = new HashSet<>();
|
||||
for (final PluginDescriptor plugin : plugins) {
|
||||
if (uniquePluginIds.add(plugin.getId()) == false) {
|
||||
throw new PluginSyncException("Duplicate plugin ID [" + plugin.getId() + "] found in [elasticsearch-plugins.yml]");
|
||||
}
|
||||
}
|
||||
|
||||
for (PluginDescriptor plugin : this.plugins) {
|
||||
if (officialPlugins.contains(plugin.getId()) == false && plugin.getLocation() == null) {
|
||||
throw new PluginSyncException(
|
||||
"Must specify location for non-official plugin [" + plugin.getId() + "] in [elasticsearch-plugins.yml]"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.proxy != null) {
|
||||
final String[] parts = this.proxy.split(":");
|
||||
if (parts.length != 2) {
|
||||
throw new PluginSyncException("Malformed [proxy], expected [host:port] in [elasticsearch-plugins.yml]");
|
||||
}
|
||||
|
||||
if (ProxyUtils.validateProxy(parts[0], parts[1]) == false) {
|
||||
throw new PluginSyncException("Malformed [proxy], expected [host:port] in [elasticsearch-plugins.yml]");
|
||||
}
|
||||
}
|
||||
|
||||
for (PluginDescriptor p : plugins) {
|
||||
if (p.getLocation() != null) {
|
||||
if (p.getLocation().isBlank()) {
|
||||
throw new PluginSyncException("Empty location for plugin [" + p.getId() + "]");
|
||||
}
|
||||
|
||||
try {
|
||||
// This also accepts Maven coordinates
|
||||
new URI(p.getLocation());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new PluginSyncException("Malformed location for plugin [" + p.getId() + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<PluginDescriptor> getPlugins() {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public String getProxy() {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
PluginsConfig that = (PluginsConfig) o;
|
||||
return plugins.equals(that.plugins) && Objects.equals(proxy, that.proxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(plugins, proxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PluginsConfig{plugins=" + plugins + ", proxy='" + proxy + "'}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@link PluginsConfig} instance from the config YAML file
|
||||
*
|
||||
* @param configPath the config file to load
|
||||
* @param xContent the XContent type to expect when reading the file
|
||||
* @return a validated config
|
||||
*/
|
||||
static PluginsConfig parseConfig(Path configPath, XContent xContent) throws IOException {
|
||||
// Normally a parser is declared and built statically in the class, but we'll only
|
||||
// use this when starting up Elasticsearch, so there's no point keeping one around.
|
||||
|
||||
final ObjectParser<PluginDescriptor, Void> descriptorParser = new ObjectParser<>("descriptor parser", PluginDescriptor::new);
|
||||
descriptorParser.declareString(PluginDescriptor::setId, new ParseField("id"));
|
||||
descriptorParser.declareStringOrNull(PluginDescriptor::setLocation, new ParseField("location"));
|
||||
|
||||
final ObjectParser<PluginsConfig, Void> parser = new ObjectParser<>("plugins parser", PluginsConfig::new);
|
||||
parser.declareStringOrNull(PluginsConfig::setProxy, new ParseField("proxy"));
|
||||
parser.declareObjectArrayOrNull(PluginsConfig::setPlugins, descriptorParser, new ParseField("plugins"));
|
||||
|
||||
final XContentParser yamlXContentParser = xContent.createParser(
|
||||
XContentParserConfiguration.EMPTY,
|
||||
Files.newInputStream(configPath)
|
||||
);
|
||||
|
||||
return parser.parse(yamlXContentParser, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a config file to disk
|
||||
* @param xContent the format to use when writing the config
|
||||
* @param config the config to write
|
||||
* @param configPath the path to write to
|
||||
* @throws IOException if anything breaks
|
||||
*/
|
||||
static void writeConfig(XContent xContent, PluginsConfig config, Path configPath) throws IOException {
|
||||
final OutputStream outputStream = Files.newOutputStream(configPath);
|
||||
final XContentBuilder builder = new XContentBuilder(xContent, outputStream);
|
||||
|
||||
builder.startObject();
|
||||
builder.startArray("plugins");
|
||||
for (PluginDescriptor p : config.getPlugins()) {
|
||||
builder.startObject();
|
||||
{
|
||||
builder.field("id", p.getId());
|
||||
builder.field("location", p.getLocation());
|
||||
}
|
||||
builder.endObject();
|
||||
}
|
||||
builder.endArray();
|
||||
builder.field("proxy", config.getProxy());
|
||||
builder.endObject();
|
||||
|
||||
builder.close();
|
||||
outputStream.close();
|
||||
}
|
||||
}
|
|
@ -20,13 +20,13 @@ import java.io.InputStream;
|
|||
*
|
||||
* Only used by the InstallPluginCommand, thus package private here
|
||||
*/
|
||||
abstract class ProgressInputStream extends FilterInputStream {
|
||||
public abstract class ProgressInputStream extends FilterInputStream {
|
||||
|
||||
private final int expectedTotalSize;
|
||||
private int currentPercent;
|
||||
private int count = 0;
|
||||
|
||||
ProgressInputStream(InputStream is, int expectedTotalSize) {
|
||||
public ProgressInputStream(InputStream is, int expectedTotalSize) {
|
||||
super(is);
|
||||
this.expectedTotalSize = expectedTotalSize;
|
||||
this.currentPercent = 0;
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.plugins.cli;
|
||||
|
||||
import org.elasticsearch.cli.ExitCodes;
|
||||
import org.elasticsearch.cli.SuppressForbidden;
|
||||
import org.elasticsearch.cli.UserException;
|
||||
import org.elasticsearch.common.Strings;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
|
||||
/**
|
||||
* Utilities for working with HTTP proxies.
|
||||
*/
|
||||
class ProxyUtils {
|
||||
/**
|
||||
* Constructs a proxy from the given string. If {@code null} is passed, then {@code null} will
|
||||
* be returned, since that is not the same as {@link Proxy#NO_PROXY}.
|
||||
*
|
||||
* @param proxy the string to use, in the form "host:port"
|
||||
* @return a proxy or null
|
||||
*/
|
||||
@SuppressForbidden(reason = "Proxy constructor requires a SocketAddress")
|
||||
static Proxy buildProxy(String proxy) throws UserException {
|
||||
if (proxy == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String[] parts = proxy.split(":");
|
||||
if (parts.length != 2) {
|
||||
throw new UserException(ExitCodes.CONFIG, "Malformed [proxy], expected [host:port]");
|
||||
}
|
||||
|
||||
if (validateProxy(parts[0], parts[1]) == false) {
|
||||
throw new UserException(ExitCodes.CONFIG, "Malformed [proxy], expected [host:port]");
|
||||
}
|
||||
|
||||
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(parts[0], Integer.parseUnsignedInt(parts[1])));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the hostname is not empty, and that the port is numeric.
|
||||
*
|
||||
* @param hostname the hostname to check. Besides ensuring it is not null or empty, no further validation is
|
||||
* performed.
|
||||
* @param port the port to check. Must be composed solely of digits.
|
||||
* @return whether the arguments describe a potentially valid proxy.
|
||||
*/
|
||||
static boolean validateProxy(String hostname, String port) {
|
||||
return Strings.isNullOrEmpty(hostname) == false && port != null && port.matches("^\\d+$") != false;
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE;
|
|||
/**
|
||||
* An action for the plugin CLI to remove plugins from Elasticsearch.
|
||||
*/
|
||||
class RemovePluginAction {
|
||||
public class RemovePluginAction {
|
||||
|
||||
// exit codes for remove
|
||||
/** A plugin cannot be removed because it is extended by another plugin. */
|
||||
|
@ -42,7 +42,7 @@ class RemovePluginAction {
|
|||
|
||||
private final Terminal terminal;
|
||||
private final Environment env;
|
||||
private final boolean purge;
|
||||
private boolean purge;
|
||||
|
||||
/**
|
||||
* Creates a new action.
|
||||
|
@ -51,12 +51,16 @@ class RemovePluginAction {
|
|||
* @param env the environment for the local node
|
||||
* @param purge if true, plugin configuration files will be removed but otherwise preserved
|
||||
*/
|
||||
RemovePluginAction(Terminal terminal, Environment env, boolean purge) {
|
||||
public RemovePluginAction(Terminal terminal, Environment env, boolean purge) {
|
||||
this.terminal = terminal;
|
||||
this.env = env;
|
||||
this.purge = purge;
|
||||
}
|
||||
|
||||
public void setPurge(boolean purge) {
|
||||
this.purge = purge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the plugin specified by {@code pluginName}.
|
||||
*
|
||||
|
@ -66,7 +70,7 @@ class RemovePluginAction {
|
|||
* @throws UserException if plugin directory does not exist
|
||||
* @throws UserException if the plugin bin directory is not a directory
|
||||
*/
|
||||
void execute(List<PluginDescriptor> plugins) throws IOException, UserException {
|
||||
public void execute(List<PluginDescriptor> plugins) throws IOException, UserException {
|
||||
if (plugins == null || plugins.isEmpty()) {
|
||||
throw new UserException(ExitCodes.USAGE, "At least one plugin ID is required");
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ class RemovePluginCommand extends EnvironmentAwareCommand {
|
|||
|
||||
@Override
|
||||
protected void execute(final Terminal terminal, final OptionSet options, final Environment env) throws Exception {
|
||||
SyncPluginsAction.ensureNoConfigFile(env);
|
||||
|
||||
final List<PluginDescriptor> plugins = arguments.values(options).stream().map(PluginDescriptor::new).collect(Collectors.toList());
|
||||
|
||||
final RemovePluginAction action = new RemovePluginAction(terminal, env, options.has(purgeOption));
|
||||
|
|
|
@ -0,0 +1,312 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.plugins.cli;
|
||||
|
||||
import org.elasticsearch.Version;
|
||||
import org.elasticsearch.cli.ExitCodes;
|
||||
import org.elasticsearch.cli.Terminal;
|
||||
import org.elasticsearch.cli.UserException;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.plugins.PluginInfo;
|
||||
import org.elasticsearch.plugins.PluginsSynchronizer;
|
||||
import org.elasticsearch.xcontent.cbor.CborXContent;
|
||||
import org.elasticsearch.xcontent.yaml.YamlXContent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Proxy;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* This action compares the contents of a configuration files, {@code elasticsearch-plugins.yml}, with the currently
|
||||
* installed plugins, and ensures that plugins are installed or removed accordingly.
|
||||
* <p>
|
||||
* This action cannot be called from the command line. It is used exclusively by Elasticsearch on startup, but only
|
||||
* if the config file exists and the distribution type allows it.
|
||||
*/
|
||||
public class SyncPluginsAction implements PluginsSynchronizer {
|
||||
public static final String ELASTICSEARCH_PLUGINS_YML = "elasticsearch-plugins.yml";
|
||||
public static final String ELASTICSEARCH_PLUGINS_YML_CACHE = ".elasticsearch-plugins.yml.cache";
|
||||
|
||||
private final Terminal terminal;
|
||||
private final Environment env;
|
||||
|
||||
public SyncPluginsAction(Terminal terminal, Environment env) {
|
||||
this.terminal = terminal;
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the plugin config file does <b>not</b> exist.
|
||||
* @param env the environment to check
|
||||
* @throws UserException if a plugins config file is found.
|
||||
*/
|
||||
public static void ensureNoConfigFile(Environment env) throws UserException {
|
||||
final Path pluginsConfig = env.configFile().resolve("elasticsearch-plugins.yml");
|
||||
if (Files.exists(pluginsConfig)) {
|
||||
throw new UserException(
|
||||
ExitCodes.USAGE,
|
||||
"Plugins config ["
|
||||
+ pluginsConfig
|
||||
+ "] exists, which is used by Elasticsearch on startup to ensure the correct plugins "
|
||||
+ "are installed. Instead of using this tool, you need to update this config file and restart Elasticsearch."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronises plugins from the config file to the plugins dir.
|
||||
*
|
||||
* @throws Exception if anything goes wrong
|
||||
*/
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
final Path configPath = this.env.configFile().resolve(ELASTICSEARCH_PLUGINS_YML);
|
||||
final Path previousConfigPath = this.env.pluginsFile().resolve(ELASTICSEARCH_PLUGINS_YML_CACHE);
|
||||
|
||||
if (Files.exists(configPath) == false) {
|
||||
// The `PluginsManager` will have checked that this file exists before invoking the action.
|
||||
throw new PluginSyncException("Plugins config does not exist: " + configPath.toAbsolutePath());
|
||||
}
|
||||
|
||||
if (Files.exists(env.pluginsFile()) == false) {
|
||||
throw new PluginSyncException("Plugins directory missing: " + env.pluginsFile());
|
||||
}
|
||||
|
||||
// Parse descriptor file
|
||||
final PluginsConfig pluginsConfig = PluginsConfig.parseConfig(configPath, YamlXContent.yamlXContent);
|
||||
pluginsConfig.validate(InstallPluginAction.OFFICIAL_PLUGINS);
|
||||
|
||||
// Parse cached descriptor file, if it exists
|
||||
final Optional<PluginsConfig> cachedPluginsConfig = Files.exists(previousConfigPath)
|
||||
? Optional.of(PluginsConfig.parseConfig(previousConfigPath, CborXContent.cborXContent))
|
||||
: Optional.empty();
|
||||
|
||||
final PluginChanges changes = getPluginChanges(pluginsConfig, cachedPluginsConfig);
|
||||
|
||||
if (changes.isEmpty()) {
|
||||
terminal.println("No plugins to install, remove or upgrade");
|
||||
return;
|
||||
}
|
||||
|
||||
performSync(pluginsConfig, changes);
|
||||
|
||||
// 8. Cached the applied config so that we can diff it on the next run.
|
||||
PluginsConfig.writeConfig(CborXContent.cborXContent, pluginsConfig, previousConfigPath);
|
||||
}
|
||||
|
||||
// @VisibleForTesting
|
||||
PluginChanges getPluginChanges(PluginsConfig pluginsConfig, Optional<PluginsConfig> cachedPluginsConfig) throws PluginSyncException {
|
||||
final List<PluginInfo> existingPlugins = getExistingPlugins(this.env);
|
||||
|
||||
final List<PluginDescriptor> pluginsThatShouldExist = pluginsConfig.getPlugins();
|
||||
final List<PluginDescriptor> pluginsThatActuallyExist = existingPlugins.stream()
|
||||
.map(info -> new PluginDescriptor(info.getName()))
|
||||
.collect(Collectors.toList());
|
||||
final Set<String> existingPluginIds = pluginsThatActuallyExist.stream().map(PluginDescriptor::getId).collect(Collectors.toSet());
|
||||
|
||||
final List<PluginDescriptor> pluginsToInstall = difference(pluginsThatShouldExist, pluginsThatActuallyExist);
|
||||
final List<PluginDescriptor> pluginsToRemove = difference(pluginsThatActuallyExist, pluginsThatShouldExist);
|
||||
|
||||
// Candidates for upgrade are any plugin that already exist and isn't about to be removed.
|
||||
final List<PluginDescriptor> pluginsToMaybeUpgrade = difference(pluginsThatShouldExist, pluginsToRemove).stream()
|
||||
.filter(each -> existingPluginIds.contains(each.getId()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
final List<PluginDescriptor> pluginsToUpgrade = getPluginsToUpgrade(pluginsToMaybeUpgrade, cachedPluginsConfig, existingPlugins);
|
||||
|
||||
return new PluginChanges(pluginsToRemove, pluginsToInstall, pluginsToUpgrade);
|
||||
}
|
||||
|
||||
private void performSync(PluginsConfig pluginsConfig, PluginChanges changes) throws Exception {
|
||||
final Proxy proxy = ProxyUtils.buildProxy(pluginsConfig.getProxy());
|
||||
|
||||
final RemovePluginAction removePluginAction = new RemovePluginAction(terminal, env, true);
|
||||
final InstallPluginAction installPluginAction = new InstallPluginAction(terminal, env, true);
|
||||
installPluginAction.setProxy(proxy);
|
||||
|
||||
performSync(installPluginAction, removePluginAction, changes);
|
||||
}
|
||||
|
||||
// @VisibleForTesting
|
||||
void performSync(InstallPluginAction installAction, RemovePluginAction removeAction, PluginChanges changes) throws Exception {
|
||||
logRequiredChanges(changes);
|
||||
|
||||
// Remove any plugins that are not in the config file
|
||||
if (changes.remove.isEmpty() == false) {
|
||||
removeAction.setPurge(true);
|
||||
removeAction.execute(changes.remove);
|
||||
}
|
||||
|
||||
// Add any plugins that are in the config file but missing from disk
|
||||
if (changes.install.isEmpty() == false) {
|
||||
installAction.execute(changes.install);
|
||||
}
|
||||
|
||||
// Upgrade plugins
|
||||
if (changes.upgrade.isEmpty() == false) {
|
||||
removeAction.setPurge(false);
|
||||
removeAction.execute(changes.upgrade);
|
||||
installAction.execute(changes.upgrade);
|
||||
}
|
||||
}
|
||||
|
||||
private List<PluginDescriptor> getPluginsToUpgrade(
|
||||
List<PluginDescriptor> pluginsToMaybeUpgrade,
|
||||
Optional<PluginsConfig> cachedPluginsConfig,
|
||||
List<PluginInfo> existingPlugins
|
||||
) {
|
||||
final Map<String, String> cachedPluginIdToLocation = cachedPluginsConfig.map(
|
||||
config -> config.getPlugins().stream().collect(Collectors.toMap(PluginDescriptor::getId, PluginDescriptor::getLocation))
|
||||
).orElse(Map.of());
|
||||
|
||||
return pluginsToMaybeUpgrade.stream().filter(eachPlugin -> {
|
||||
final String eachPluginId = eachPlugin.getId();
|
||||
|
||||
// If a plugin's location has changed, reinstall
|
||||
if (Objects.equals(eachPlugin.getLocation(), cachedPluginIdToLocation.get(eachPluginId)) == false) {
|
||||
this.terminal.println(
|
||||
Terminal.Verbosity.VERBOSE,
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"Location for plugin [%s] has changed from [%s] to [%s], reinstalling",
|
||||
eachPluginId,
|
||||
cachedPluginIdToLocation.get(eachPluginId),
|
||||
eachPlugin.getLocation()
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Official plugins must be upgraded when an Elasticsearch node is upgraded.
|
||||
if (InstallPluginAction.OFFICIAL_PLUGINS.contains(eachPluginId)) {
|
||||
// Find the currently installed plugin and check whether the version is lower than
|
||||
// the current node's version.
|
||||
final PluginInfo info = existingPlugins.stream()
|
||||
.filter(each -> each.getName().equals(eachPluginId))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> {
|
||||
// It should be literally impossible for us not to find a matching existing plugin. We derive
|
||||
// the list of existing plugin IDs from the list of installed plugins.
|
||||
throw new RuntimeException("Couldn't find a PluginInfo for [" + eachPluginId + "], which should be impossible");
|
||||
});
|
||||
|
||||
if (info.getElasticsearchVersion().before(Version.CURRENT)) {
|
||||
this.terminal.println(
|
||||
Terminal.Verbosity.VERBOSE,
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"Official plugin [%s] is out-of-date (%s versus %s), upgrading",
|
||||
eachPluginId,
|
||||
info.getElasticsearchVersion(),
|
||||
Version.CURRENT
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Else don't upgrade.
|
||||
return false;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<PluginInfo> getExistingPlugins(Environment env) throws PluginSyncException {
|
||||
final List<PluginInfo> plugins = new ArrayList<>();
|
||||
|
||||
try {
|
||||
try (DirectoryStream<Path> paths = Files.newDirectoryStream(env.pluginsFile())) {
|
||||
for (Path pluginPath : paths) {
|
||||
String filename = pluginPath.getFileName().toString();
|
||||
if (filename.startsWith(".")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PluginInfo info = PluginInfo.readFromProperties(env.pluginsFile().resolve(pluginPath));
|
||||
plugins.add(info);
|
||||
|
||||
// Check for a version mismatch, unless it's an official plugin since we can upgrade them.
|
||||
if (InstallPluginAction.OFFICIAL_PLUGINS.contains(info.getName())
|
||||
&& info.getElasticsearchVersion().equals(Version.CURRENT) == false) {
|
||||
this.terminal.errorPrintln(
|
||||
String.format(
|
||||
Locale.ROOT,
|
||||
"WARNING: plugin [%s] was built for Elasticsearch version %s but version %s is required",
|
||||
info.getName(),
|
||||
info.getElasticsearchVersion(),
|
||||
Version.CURRENT
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new PluginSyncException("Failed to list existing plugins", e);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all elements in {@code left} that are not present in {@code right}.
|
||||
* <p>
|
||||
* Comparisons are based solely using {@link PluginDescriptor#getId()}.
|
||||
*
|
||||
* @param left the items that may be retained
|
||||
* @param right the items that may be removed
|
||||
* @return a list of the remaining elements
|
||||
*/
|
||||
private static List<PluginDescriptor> difference(List<PluginDescriptor> left, List<PluginDescriptor> right) {
|
||||
return left.stream().filter(eachDescriptor -> {
|
||||
final String id = eachDescriptor.getId();
|
||||
return right.stream().anyMatch(p -> p.getId().equals(id)) == false;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void logRequiredChanges(PluginChanges changes) {
|
||||
final BiConsumer<String, List<PluginDescriptor>> printSummary = (action, plugins) -> {
|
||||
if (plugins.isEmpty() == false) {
|
||||
List<String> pluginIds = plugins.stream().map(PluginDescriptor::getId).collect(Collectors.toList());
|
||||
this.terminal.errorPrintln(String.format(Locale.ROOT, "Plugins to be %s: %s", action, pluginIds));
|
||||
}
|
||||
};
|
||||
|
||||
printSummary.accept("removed", changes.remove);
|
||||
printSummary.accept("installed", changes.install);
|
||||
printSummary.accept("upgraded", changes.upgrade);
|
||||
}
|
||||
|
||||
// @VisibleForTesting
|
||||
static class PluginChanges {
|
||||
final List<PluginDescriptor> remove;
|
||||
final List<PluginDescriptor> install;
|
||||
final List<PluginDescriptor> upgrade;
|
||||
|
||||
PluginChanges(List<PluginDescriptor> remove, List<PluginDescriptor> install, List<PluginDescriptor> upgrade) {
|
||||
this.remove = Objects.requireNonNull(remove);
|
||||
this.install = Objects.requireNonNull(install);
|
||||
this.upgrade = Objects.requireNonNull(upgrade);
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return remove.isEmpty() && install.isEmpty() && upgrade.isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -221,7 +221,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
Path zip = createTempDir().resolve(structure.getFileName() + ".zip");
|
||||
try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zip))) {
|
||||
forEachFileRecursively(structure, (file, attrs) -> {
|
||||
String target = (prefix == null ? "" : prefix + "/") + structure.relativize(file).toString();
|
||||
String target = (prefix == null ? "" : prefix + "/") + structure.relativize(file);
|
||||
stream.putNextEntry(new ZipEntry(target));
|
||||
Files.copy(file, stream);
|
||||
});
|
||||
|
@ -415,20 +415,20 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
public void testDuplicateInstall() throws Exception {
|
||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||
final UserException e = expectThrows(UserException.class, () -> installPlugins(List.of(pluginZip, pluginZip), env.v1()));
|
||||
assertThat(e, hasToString(containsString("duplicate plugin id [" + pluginZip.getId() + "]")));
|
||||
assertThat(e.getMessage(), equalTo("duplicate plugin id [" + pluginZip.getId() + "]"));
|
||||
}
|
||||
|
||||
public void testTransaction() throws Exception {
|
||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||
PluginDescriptor nonexistentPluginZip = new PluginDescriptor(
|
||||
pluginZip.getId() + "-does-not-exist",
|
||||
pluginZip.getUrl() + "-does-not-exist"
|
||||
pluginZip.getLocation() + "-does-not-exist"
|
||||
);
|
||||
final FileNotFoundException e = expectThrows(
|
||||
FileNotFoundException.class,
|
||||
() -> installPlugins(List.of(pluginZip, nonexistentPluginZip), env.v1())
|
||||
);
|
||||
assertThat(e, hasToString(containsString("does-not-exist")));
|
||||
assertThat(e.getMessage(), containsString("does-not-exist"));
|
||||
final Path fakeInstallPath = env.v2().pluginsFile().resolve("fake");
|
||||
// fake should have been removed when the file not found exception occurred
|
||||
assertFalse(Files.exists(fakeInstallPath));
|
||||
|
@ -445,13 +445,13 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
"found file [%s] from a failed attempt to remove the plugin [failed]; execute [elasticsearch-plugin remove failed]",
|
||||
removing
|
||||
);
|
||||
assertThat(e, hasToString(containsString(expected)));
|
||||
assertThat(e.getMessage(), containsString(expected));
|
||||
}
|
||||
|
||||
public void testSpaceInUrl() throws Exception {
|
||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||
Path pluginZipWithSpaces = createTempFile("foo bar", ".zip");
|
||||
try (InputStream in = FileSystemUtils.openFileURLStream(new URL(pluginZip.getUrl()))) {
|
||||
try (InputStream in = FileSystemUtils.openFileURLStream(new URL(pluginZip.getLocation()))) {
|
||||
Files.copy(in, pluginZipWithSpaces, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
PluginDescriptor modifiedPlugin = new PluginDescriptor("fake", pluginZipWithSpaces.toUri().toURL().toString());
|
||||
|
@ -463,7 +463,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
// has two colons, so it appears similar to maven coordinates
|
||||
PluginDescriptor plugin = new PluginDescriptor("fake", "://host:1234");
|
||||
MalformedURLException e = expectThrows(MalformedURLException.class, () -> installPlugin(plugin));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("no protocol"));
|
||||
assertThat(e.getMessage(), containsString("no protocol"));
|
||||
}
|
||||
|
||||
public void testFileNotMaven() {
|
||||
|
@ -473,13 +473,13 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
// has two colons, so it appears similar to maven coordinates
|
||||
() -> installPlugin("file:" + dir)
|
||||
);
|
||||
assertFalse(e.getMessage(), e.getMessage().contains("maven.org"));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains(dir));
|
||||
assertThat(e.getMessage(), not(containsString("maven.org")));
|
||||
assertThat(e.getMessage(), containsString(dir));
|
||||
}
|
||||
|
||||
public void testUnknownPlugin() {
|
||||
UserException e = expectThrows(UserException.class, () -> installPlugin("foo"));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("Unknown plugin foo"));
|
||||
assertThat(e.getMessage(), containsString("Unknown plugin foo"));
|
||||
}
|
||||
|
||||
public void testPluginsDirReadOnly() throws Exception {
|
||||
|
@ -488,7 +488,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
pluginsAttrs.setPermissions(new HashSet<>());
|
||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||
IOException e = expectThrows(IOException.class, () -> installPlugin(pluginZip));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains(env.v2().pluginsFile().toString()));
|
||||
assertThat(e.getMessage(), containsString(env.v2().pluginsFile().toString()));
|
||||
}
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
@ -496,7 +496,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
public void testBuiltinModule() throws Exception {
|
||||
PluginDescriptor pluginZip = createPluginZip("lang-painless", pluginDir);
|
||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("is a system module"));
|
||||
assertThat(e.getMessage(), containsString("is a system module"));
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
||||
|
@ -506,7 +506,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
// whose descriptor contains the name "x-pack".
|
||||
pluginZip.setId("not-x-pack");
|
||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("is a system module"));
|
||||
assertThat(e.getMessage(), containsString("is a system module"));
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
||||
|
@ -517,7 +517,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
writeJar(pluginDirectory.resolve("other.jar"), "FakePlugin");
|
||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDirectory); // adds plugin.jar with FakePlugin
|
||||
IllegalStateException e = expectThrows(IllegalStateException.class, () -> installPlugin(pluginZip, env.v1(), defaultAction));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("jar hell"));
|
||||
assertThat(e.getMessage(), containsString("jar hell"));
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
||||
|
@ -537,7 +537,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||
installPlugin(pluginZip);
|
||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("already exists"));
|
||||
assertThat(e.getMessage(), containsString("already exists"));
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
||||
|
@ -555,7 +555,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
Files.createFile(binDir);
|
||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("not a directory"));
|
||||
assertThat(e.getMessage(), containsString("not a directory"));
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
||||
|
@ -565,7 +565,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
Files.createFile(dirInBinDir.resolve("somescript"));
|
||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("Directories not allowed in bin dir for plugin"));
|
||||
assertThat(e.getMessage(), containsString("Directories not allowed in bin dir for plugin"));
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
||||
|
@ -575,7 +575,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
Files.createFile(binDir.resolve("somescript"));
|
||||
PluginDescriptor pluginZip = createPluginZip("elasticsearch", pluginDir);
|
||||
FileAlreadyExistsException e = expectThrows(FileAlreadyExistsException.class, () -> installPlugin(pluginZip));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains(env.v2().binFile().resolve("elasticsearch").toString()));
|
||||
assertThat(e.getMessage(), containsString(env.v2().binFile().resolve("elasticsearch").toString()));
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
||||
|
@ -687,7 +687,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
Files.createFile(configDir);
|
||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("not a directory"));
|
||||
assertThat(e.getMessage(), containsString("not a directory"));
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
||||
|
@ -697,7 +697,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
Files.createFile(dirInConfigDir.resolve("myconfig.yml"));
|
||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("Directories not allowed in config dir for plugin"));
|
||||
assertThat(e.getMessage(), containsString("Directories not allowed in config dir for plugin"));
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
||||
|
@ -705,7 +705,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
Files.createFile(pluginDir.resolve("fake.yml"));
|
||||
String pluginZip = writeZip(pluginDir, null).toUri().toURL().toString();
|
||||
NoSuchFileException e = expectThrows(NoSuchFileException.class, () -> installPlugin(pluginZip));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("plugin-descriptor.properties"));
|
||||
assertThat(e.getMessage(), containsString("plugin-descriptor.properties"));
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
||||
|
@ -724,18 +724,13 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
}
|
||||
String pluginZip = zip.toUri().toURL().toString();
|
||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("resolving outside of plugin directory"));
|
||||
assertThat(e.getMessage(), containsString("resolving outside of plugin directory"));
|
||||
assertInstallCleaned(env.v2());
|
||||
}
|
||||
|
||||
public void testOfficialPluginsHelpSortedAndMissingObviouslyWrongPlugins() throws Exception {
|
||||
MockTerminal terminal = new MockTerminal();
|
||||
new InstallPluginCommand() {
|
||||
@Override
|
||||
protected boolean addShutdownHook() {
|
||||
return false;
|
||||
}
|
||||
}.main(new String[] { "--help" }, terminal);
|
||||
new MockInstallPluginCommand().main(new String[] { "--help" }, terminal);
|
||||
try (BufferedReader reader = new BufferedReader(new StringReader(terminal.getOutput()))) {
|
||||
String line = reader.readLine();
|
||||
|
||||
|
@ -779,7 +774,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
}
|
||||
};
|
||||
final T exception = expectThrows(clazz, () -> flavorAction.execute(List.of(new PluginDescriptor("x-pack"))));
|
||||
assertThat(exception, hasToString(containsString(expectedMessage)));
|
||||
assertThat(exception.getMessage(), containsString(expectedMessage));
|
||||
}
|
||||
|
||||
public void testInstallMisspelledOfficialPlugins() {
|
||||
|
@ -831,6 +826,18 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that if the installer action finds a mismatch between what it expects a plugin's ID to be and what
|
||||
* the ID actually is from the plugin's properties, then the installation fails.
|
||||
*/
|
||||
public void testPluginHasDifferentNameThatDescriptor() throws Exception {
|
||||
PluginDescriptor descriptor = createPluginZip("fake", pluginDir);
|
||||
PluginDescriptor modifiedDescriptor = new PluginDescriptor("other-fake", descriptor.getLocation());
|
||||
|
||||
final UserException e = expectThrows(UserException.class, () -> installPlugin(modifiedDescriptor));
|
||||
assertThat(e.getMessage(), equalTo("Expected downloaded plugin to have ID [other-fake] but found [fake]"));
|
||||
}
|
||||
|
||||
private void installPlugin(boolean isBatch, String... additionalProperties) throws Exception {
|
||||
// if batch is enabled, we also want to add a security policy
|
||||
if (isBatch) {
|
||||
|
@ -842,10 +849,36 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
skipJarHellAction.execute(List.of(pluginZip));
|
||||
}
|
||||
|
||||
private void assertInstallPluginFromUrl(final String pluginId, final String url, final String stagingHash, boolean isSnapshot)
|
||||
throws Exception {
|
||||
assertInstallPluginFromUrl(pluginId, null, url, stagingHash, isSnapshot);
|
||||
}
|
||||
|
||||
private void assertInstallPluginFromUrl(
|
||||
final String pluginId,
|
||||
final String pluginUrl,
|
||||
final String url,
|
||||
final String stagingHash,
|
||||
boolean isSnapshot
|
||||
) throws Exception {
|
||||
final MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
assertInstallPluginFromUrl(
|
||||
pluginId,
|
||||
pluginUrl,
|
||||
url,
|
||||
stagingHash,
|
||||
isSnapshot,
|
||||
".sha512",
|
||||
checksumAndFilename(digest, url),
|
||||
newSecretKey(),
|
||||
this::signature
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressForbidden(reason = "Path.of() is OK in this context")
|
||||
void assertInstallPluginFromUrl(
|
||||
final String pluginId,
|
||||
final String name,
|
||||
final String pluginUrl,
|
||||
final String url,
|
||||
final String stagingHash,
|
||||
final boolean isSnapshot,
|
||||
|
@ -854,8 +887,8 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
final PGPSecretKey secretKey,
|
||||
final BiFunction<byte[], PGPSecretKey, String> signature
|
||||
) throws Exception {
|
||||
PluginDescriptor pluginZip = createPlugin(name, pluginDir);
|
||||
Path pluginZipPath = Path.of(URI.create(pluginZip.getUrl()));
|
||||
PluginDescriptor pluginZip = createPlugin(pluginId, pluginDir);
|
||||
Path pluginZipPath = Path.of(URI.create(pluginZip.getLocation()));
|
||||
InstallPluginAction action = new InstallPluginAction(terminal, env.v2(), false) {
|
||||
@Override
|
||||
Path downloadZip(String urlString, Path tmpDir) throws IOException {
|
||||
|
@ -868,7 +901,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
@Override
|
||||
URL openUrl(String urlString) throws IOException {
|
||||
if ((url + shaExtension).equals(urlString)) {
|
||||
// calc sha an return file URL to it
|
||||
// calc sha and return file URL to it
|
||||
Path shaFile = temp.apply("shas").resolve("downloaded.zip" + shaExtension);
|
||||
byte[] zipbytes = Files.readAllBytes(pluginZipPath);
|
||||
String checksum = shaCalculator.apply(zipbytes);
|
||||
|
@ -886,7 +919,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
|
||||
@Override
|
||||
void verifySignature(Path zip, String urlString) throws IOException, PGPException {
|
||||
if (InstallPluginAction.OFFICIAL_PLUGINS.contains(name)) {
|
||||
if (InstallPluginAction.OFFICIAL_PLUGINS.contains(pluginId)) {
|
||||
super.verifySignature(zip, urlString);
|
||||
} else {
|
||||
throw new UnsupportedOperationException("verify signature should not be called for unofficial plugins");
|
||||
|
@ -936,36 +969,15 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
// no jarhell check
|
||||
}
|
||||
};
|
||||
installPlugin(new PluginDescriptor(name, pluginId), env.v1(), action);
|
||||
assertPlugin(name, pluginDir, env.v2());
|
||||
}
|
||||
|
||||
public void assertInstallPluginFromUrl(
|
||||
final String pluginId,
|
||||
final String name,
|
||||
final String url,
|
||||
final String stagingHash,
|
||||
boolean isSnapshot
|
||||
) throws Exception {
|
||||
final MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
assertInstallPluginFromUrl(
|
||||
pluginId,
|
||||
name,
|
||||
url,
|
||||
stagingHash,
|
||||
isSnapshot,
|
||||
".sha512",
|
||||
checksumAndFilename(digest, url),
|
||||
newSecretKey(),
|
||||
this::signature
|
||||
);
|
||||
installPlugin(new PluginDescriptor(pluginId, pluginUrl), env.v1(), action);
|
||||
assertPlugin(pluginId, pluginDir, env.v2());
|
||||
}
|
||||
|
||||
public void testOfficialPlugin() throws Exception {
|
||||
String url = "https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-"
|
||||
+ Build.CURRENT.getQualifiedVersion()
|
||||
+ ".zip";
|
||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, false);
|
||||
assertInstallPluginFromUrl("analysis-icu", url, null, false);
|
||||
}
|
||||
|
||||
public void testOfficialPluginSnapshot() throws Exception {
|
||||
|
@ -975,7 +987,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
Version.CURRENT,
|
||||
Build.CURRENT.getQualifiedVersion()
|
||||
);
|
||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, "abc123", true);
|
||||
assertInstallPluginFromUrl("analysis-icu", url, "abc123", true);
|
||||
}
|
||||
|
||||
public void testInstallReleaseBuildOfPluginOnSnapshotBuild() {
|
||||
|
@ -985,15 +997,15 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
Version.CURRENT,
|
||||
Build.CURRENT.getQualifiedVersion()
|
||||
);
|
||||
// attemping to install a release build of a plugin (no staging ID) on a snapshot build should throw a user exception
|
||||
// attempting to install a release build of a plugin (no staging ID) on a snapshot build should throw a user exception
|
||||
final UserException e = expectThrows(
|
||||
UserException.class,
|
||||
() -> assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, true)
|
||||
);
|
||||
assertThat(e.exitCode, equalTo(ExitCodes.CONFIG));
|
||||
assertThat(
|
||||
e,
|
||||
hasToString(containsString("attempted to install release build of official plugin on snapshot build of Elasticsearch"))
|
||||
e.getMessage(),
|
||||
containsString("attempted to install release build of official plugin on snapshot build of Elasticsearch")
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1003,7 +1015,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
+ "-abc123/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-"
|
||||
+ Build.CURRENT.getQualifiedVersion()
|
||||
+ ".zip";
|
||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, "abc123", false);
|
||||
assertInstallPluginFromUrl("analysis-icu", url, "abc123", false);
|
||||
}
|
||||
|
||||
public void testOfficialPlatformPlugin() throws Exception {
|
||||
|
@ -1012,7 +1024,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
+ "-"
|
||||
+ Build.CURRENT.getQualifiedVersion()
|
||||
+ ".zip";
|
||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, false);
|
||||
assertInstallPluginFromUrl("analysis-icu", url, null, false);
|
||||
}
|
||||
|
||||
public void testOfficialPlatformPluginSnapshot() throws Exception {
|
||||
|
@ -1023,7 +1035,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
Platforms.PLATFORM_NAME,
|
||||
Build.CURRENT.getQualifiedVersion()
|
||||
);
|
||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, "abc123", true);
|
||||
assertInstallPluginFromUrl("analysis-icu", url, "abc123", true);
|
||||
}
|
||||
|
||||
public void testOfficialPlatformPluginStaging() throws Exception {
|
||||
|
@ -1034,23 +1046,23 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
+ "-"
|
||||
+ Build.CURRENT.getQualifiedVersion()
|
||||
+ ".zip";
|
||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, "abc123", false);
|
||||
assertInstallPluginFromUrl("analysis-icu", url, "abc123", false);
|
||||
}
|
||||
|
||||
public void testMavenPlugin() throws Exception {
|
||||
String url = "https://repo1.maven.org/maven2/mygroup/myplugin/1.0.0/myplugin-1.0.0.zip";
|
||||
assertInstallPluginFromUrl("mygroup:myplugin:1.0.0", "myplugin", url, null, false);
|
||||
assertInstallPluginFromUrl("myplugin", "mygroup:myplugin:1.0.0", url, null, false);
|
||||
}
|
||||
|
||||
public void testMavenPlatformPlugin() throws Exception {
|
||||
String url = "https://repo1.maven.org/maven2/mygroup/myplugin/1.0.0/myplugin-" + Platforms.PLATFORM_NAME + "-1.0.0.zip";
|
||||
assertInstallPluginFromUrl("mygroup:myplugin:1.0.0", "myplugin", url, null, false);
|
||||
assertInstallPluginFromUrl("myplugin", "mygroup:myplugin:1.0.0", url, null, false);
|
||||
}
|
||||
|
||||
public void testMavenSha1Backcompat() throws Exception {
|
||||
String url = "https://repo1.maven.org/maven2/mygroup/myplugin/1.0.0/myplugin-1.0.0.zip";
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
assertInstallPluginFromUrl("mygroup:myplugin:1.0.0", "myplugin", url, null, false, ".sha1", checksum(digest), null, (b, p) -> null);
|
||||
assertInstallPluginFromUrl("myplugin", "mygroup:myplugin:1.0.0", url, null, false, ".sha1", checksum(digest), null, (b, p) -> null);
|
||||
assertTrue(terminal.getOutput(), terminal.getOutput().contains("sha512 not found, falling back to sha1"));
|
||||
}
|
||||
|
||||
|
@ -1058,8 +1070,8 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
String url = "https://repo1.maven.org/maven2/mygroup/myplugin/1.0.0/myplugin-1.0.0.zip";
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
assertInstallPluginFromUrl(
|
||||
"mygroup:myplugin:1.0.0",
|
||||
"myplugin",
|
||||
"mygroup:myplugin:1.0.0",
|
||||
url,
|
||||
null,
|
||||
false,
|
||||
|
@ -1077,17 +1089,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
UserException e = expectThrows(
|
||||
UserException.class,
|
||||
() -> assertInstallPluginFromUrl(
|
||||
"analysis-icu",
|
||||
"analysis-icu",
|
||||
url,
|
||||
null,
|
||||
false,
|
||||
".sha512",
|
||||
checksum(digest),
|
||||
null,
|
||||
(b, p) -> null
|
||||
)
|
||||
() -> assertInstallPluginFromUrl("analysis-icu", null, url, null, false, ".sha512", checksum(digest), null, (b, p) -> null)
|
||||
);
|
||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
||||
assertThat(e.getMessage(), startsWith("Invalid checksum file"));
|
||||
|
@ -1100,20 +1102,10 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
UserException e = expectThrows(
|
||||
UserException.class,
|
||||
() -> assertInstallPluginFromUrl(
|
||||
"analysis-icu",
|
||||
"analysis-icu",
|
||||
url,
|
||||
null,
|
||||
false,
|
||||
".sha1",
|
||||
checksum(digest),
|
||||
null,
|
||||
(b, p) -> null
|
||||
)
|
||||
() -> assertInstallPluginFromUrl("analysis-icu", null, url, null, false, ".sha1", checksum(digest), null, (b, p) -> null)
|
||||
);
|
||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
||||
assertEquals("Plugin checksum missing: " + url + ".sha512", e.getMessage());
|
||||
assertThat(e.getMessage(), equalTo("Plugin checksum missing: " + url + ".sha512"));
|
||||
}
|
||||
|
||||
public void testMavenShaMissing() {
|
||||
|
@ -1121,8 +1113,8 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
UserException e = expectThrows(
|
||||
UserException.class,
|
||||
() -> assertInstallPluginFromUrl(
|
||||
"mygroup:myplugin:1.0.0",
|
||||
"myplugin",
|
||||
"mygroup:myplugin:1.0.0",
|
||||
url,
|
||||
null,
|
||||
false,
|
||||
|
@ -1133,7 +1125,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
)
|
||||
);
|
||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
||||
assertEquals("Plugin checksum missing: " + url + ".sha1", e.getMessage());
|
||||
assertThat(e.getMessage(), equalTo("Plugin checksum missing: " + url + ".sha1"));
|
||||
}
|
||||
|
||||
public void testInvalidShaFileMissingFilename() throws Exception {
|
||||
|
@ -1143,20 +1135,10 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
UserException e = expectThrows(
|
||||
UserException.class,
|
||||
() -> assertInstallPluginFromUrl(
|
||||
"analysis-icu",
|
||||
"analysis-icu",
|
||||
url,
|
||||
null,
|
||||
false,
|
||||
".sha512",
|
||||
checksum(digest),
|
||||
null,
|
||||
(b, p) -> null
|
||||
)
|
||||
() -> assertInstallPluginFromUrl("analysis-icu", null, url, null, false, ".sha512", checksum(digest), null, (b, p) -> null)
|
||||
);
|
||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
||||
assertTrue(e.getMessage(), e.getMessage().startsWith("Invalid checksum file"));
|
||||
assertThat(e.getMessage(), containsString("Invalid checksum file"));
|
||||
}
|
||||
|
||||
public void testInvalidShaFileMismatchFilename() throws Exception {
|
||||
|
@ -1168,7 +1150,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
UserException.class,
|
||||
() -> assertInstallPluginFromUrl(
|
||||
"analysis-icu",
|
||||
"analysis-icu",
|
||||
null,
|
||||
url,
|
||||
null,
|
||||
false,
|
||||
|
@ -1191,7 +1173,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
UserException.class,
|
||||
() -> assertInstallPluginFromUrl(
|
||||
"analysis-icu",
|
||||
"analysis-icu",
|
||||
null,
|
||||
url,
|
||||
null,
|
||||
false,
|
||||
|
@ -1202,7 +1184,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
)
|
||||
);
|
||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
||||
assertTrue(e.getMessage(), e.getMessage().startsWith("Invalid checksum file"));
|
||||
assertThat(e.getMessage(), containsString("Invalid checksum file"));
|
||||
}
|
||||
|
||||
public void testSha512Mismatch() {
|
||||
|
@ -1213,7 +1195,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
UserException.class,
|
||||
() -> assertInstallPluginFromUrl(
|
||||
"analysis-icu",
|
||||
"analysis-icu",
|
||||
null,
|
||||
url,
|
||||
null,
|
||||
false,
|
||||
|
@ -1224,7 +1206,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
)
|
||||
);
|
||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("SHA-512 mismatch, expected foobar"));
|
||||
assertThat(e.getMessage(), containsString("SHA-512 mismatch, expected foobar"));
|
||||
}
|
||||
|
||||
public void testSha1Mismatch() {
|
||||
|
@ -1232,8 +1214,8 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
UserException e = expectThrows(
|
||||
UserException.class,
|
||||
() -> assertInstallPluginFromUrl(
|
||||
"mygroup:myplugin:1.0.0",
|
||||
"myplugin",
|
||||
"mygroup:myplugin:1.0.0",
|
||||
url,
|
||||
null,
|
||||
false,
|
||||
|
@ -1244,7 +1226,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
)
|
||||
);
|
||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("SHA-1 mismatch, expected foobar"));
|
||||
assertThat(e.getMessage(), containsString("SHA-1 mismatch, expected foobar"));
|
||||
}
|
||||
|
||||
public void testPublicKeyIdMismatchToExpectedPublicKeyId() throws Exception {
|
||||
|
@ -1269,7 +1251,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
IllegalStateException.class,
|
||||
() -> assertInstallPluginFromUrl(
|
||||
icu,
|
||||
icu,
|
||||
null,
|
||||
url,
|
||||
null,
|
||||
false,
|
||||
|
@ -1279,7 +1261,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
signature
|
||||
)
|
||||
);
|
||||
assertThat(e, hasToString(containsString("key id [" + actualID + "] does not match expected key id [" + expectedID + "]")));
|
||||
assertThat(e.getMessage(), containsString("key id [" + actualID + "] does not match expected key id [" + expectedID + "]"));
|
||||
}
|
||||
|
||||
public void testFailedSignatureVerification() throws Exception {
|
||||
|
@ -1304,7 +1286,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
IllegalStateException.class,
|
||||
() -> assertInstallPluginFromUrl(
|
||||
icu,
|
||||
icu,
|
||||
null,
|
||||
url,
|
||||
null,
|
||||
false,
|
||||
|
@ -1314,7 +1296,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
signature
|
||||
)
|
||||
);
|
||||
assertThat(e, hasToString(equalTo("java.lang.IllegalStateException: signature verification for [" + url + "] failed")));
|
||||
assertThat(e.getMessage(), containsString("signature verification for [" + url + "] failed"));
|
||||
}
|
||||
|
||||
public PGPSecretKey newSecretKey() throws NoSuchAlgorithmException, PGPException {
|
||||
|
@ -1370,7 +1352,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
}
|
||||
generator.generate().encode(pout);
|
||||
}
|
||||
return new String(output.toByteArray(), "UTF-8");
|
||||
return output.toString(StandardCharsets.UTF_8);
|
||||
} catch (IOException | PGPException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
@ -1387,7 +1369,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
// default answer, does not install
|
||||
terminal.addTextInput("");
|
||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
||||
assertEquals("installation aborted by user", e.getMessage());
|
||||
assertThat(e.getMessage(), containsString("installation aborted by user"));
|
||||
|
||||
assertThat(terminal.getErrorOutput(), containsString("WARNING: " + warning));
|
||||
try (Stream<Path> fileStream = Files.list(env.v2().pluginsFile())) {
|
||||
|
@ -1401,7 +1383,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
}
|
||||
terminal.addTextInput("n");
|
||||
e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
||||
assertEquals("installation aborted by user", e.getMessage());
|
||||
assertThat(e.getMessage(), containsString("installation aborted by user"));
|
||||
assertThat(terminal.getErrorOutput(), containsString("WARNING: " + warning));
|
||||
try (Stream<Path> fileStream = Files.list(env.v2().pluginsFile())) {
|
||||
assertThat(fileStream.collect(Collectors.toList()), empty());
|
||||
|
@ -1431,7 +1413,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
|||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir, "has.native.controller", "true");
|
||||
|
||||
final IllegalStateException e = expectThrows(IllegalStateException.class, () -> installPlugin(pluginZip));
|
||||
assertThat(e, hasToString(containsString("plugins can not have native controllers")));
|
||||
assertThat(e.getMessage(), containsString("plugins can not have native controllers"));
|
||||
}
|
||||
|
||||
public void testMultipleJars() throws Exception {
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.plugins.cli;
|
||||
|
||||
import org.elasticsearch.cli.UserException;
|
||||
import org.elasticsearch.env.Environment;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class MockInstallPluginCommand extends InstallPluginCommand {
|
||||
private final Environment env;
|
||||
|
||||
public MockInstallPluginCommand(Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
public MockInstallPluginCommand() {
|
||||
this.env = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Environment createEnv(Map<String, String> settings) throws UserException {
|
||||
return this.env != null ? this.env : super.createEnv(settings);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean addShutdownHook() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.plugins.cli;
|
||||
|
||||
import org.elasticsearch.env.Environment;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class MockRemovePluginCommand extends RemovePluginCommand {
|
||||
final Environment env;
|
||||
|
||||
public MockRemovePluginCommand(final Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Environment createEnv(Map<String, String> settings) {
|
||||
return env;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.plugins.cli;
|
||||
|
||||
import org.elasticsearch.cli.SuppressForbidden;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
|
||||
class ProxyMatcher extends TypeSafeMatcher<Proxy> {
|
||||
private final Proxy.Type type;
|
||||
private final String hostname;
|
||||
private final int port;
|
||||
|
||||
public static ProxyMatcher matchesProxy(Proxy.Type type, String hostname, int port) {
|
||||
return new ProxyMatcher(type, hostname, port);
|
||||
}
|
||||
|
||||
public static ProxyMatcher matchesProxy(Proxy.Type type) {
|
||||
return new ProxyMatcher(type, null, -1);
|
||||
}
|
||||
|
||||
ProxyMatcher(Proxy.Type type, String hostname, int port) {
|
||||
this.type = type;
|
||||
this.hostname = hostname;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressForbidden(reason = "Proxy constructor uses InetSocketAddress")
|
||||
protected boolean matchesSafely(Proxy proxy) {
|
||||
if (proxy.type() != this.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hostname == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
InetSocketAddress address = (InetSocketAddress) proxy.address();
|
||||
|
||||
return this.hostname.equals(address.getHostName()) && this.port == address.getPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("a proxy instance of type [" + type + "] pointing at [" + hostname + ":" + port + "]");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.plugins.cli;
|
||||
|
||||
import org.elasticsearch.cli.UserException;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
|
||||
import java.net.Proxy.Type;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.elasticsearch.plugins.cli.ProxyMatcher.matchesProxy;
|
||||
import static org.elasticsearch.plugins.cli.ProxyUtils.buildProxy;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
|
||||
public class ProxyUtilsTests extends ESTestCase {
|
||||
/**
|
||||
* Check that building a proxy with just a hostname and port succeeds.
|
||||
*/
|
||||
public void testBuildProxy_withHostPort() throws Exception {
|
||||
assertThat(buildProxy("host:1234"), matchesProxy(Type.HTTP, "host", 1234));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that building a proxy with a null value succeeds, returning a pass-through (direct) proxy.
|
||||
*/
|
||||
public void testBuildProxy_withNullValue() throws Exception {
|
||||
assertThat(buildProxy(null), is(nullValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that building a proxy with a missing host is rejected.
|
||||
*/
|
||||
public void testBuildProxy_withMissingHost() {
|
||||
UserException e = expectThrows(UserException.class, () -> buildProxy(":1234"));
|
||||
assertThat(e.getMessage(), equalTo("Malformed [proxy], expected [host:port]"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that building a proxy with a missing or invalid port is rejected.
|
||||
*/
|
||||
public void testBuildProxy_withInvalidPort() {
|
||||
Stream.of("host:", "host.domain:-1", "host.domain:$PORT", "host.domain:{{port}}", "host.domain").forEach(testCase -> {
|
||||
UserException e = expectThrows(UserException.class, () -> buildProxy(testCase));
|
||||
assertThat(e.getMessage(), equalTo("Malformed [proxy], expected [host:port]"));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -28,7 +28,6 @@ import java.nio.file.DirectoryStream;
|
|||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.Collections.emptyList;
|
||||
|
@ -36,7 +35,6 @@ import static org.hamcrest.CoreMatchers.containsString;
|
|||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.hamcrest.CoreMatchers.nullValue;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasToString;
|
||||
|
||||
@LuceneTestCase.SuppressFileSystems("*")
|
||||
public class RemovePluginActionTests extends ESTestCase {
|
||||
|
@ -44,19 +42,6 @@ public class RemovePluginActionTests extends ESTestCase {
|
|||
private Path home;
|
||||
private Environment env;
|
||||
|
||||
static class MockRemovePluginCommand extends RemovePluginCommand {
|
||||
final Environment env;
|
||||
|
||||
private MockRemovePluginCommand(final Environment env) {
|
||||
this.env = env;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Environment createEnv(Map<String, String> settings) throws UserException {
|
||||
return env;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
@ -121,7 +106,7 @@ public class RemovePluginActionTests extends ESTestCase {
|
|||
|
||||
public void testMissing() throws Exception {
|
||||
UserException e = expectThrows(UserException.class, () -> removePlugin("dne", home, randomBoolean()));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("plugin [dne] not found"));
|
||||
assertThat(e.getMessage(), containsString("plugin [dne] not found"));
|
||||
assertRemoveCleaned(env);
|
||||
}
|
||||
|
||||
|
@ -182,7 +167,7 @@ public class RemovePluginActionTests extends ESTestCase {
|
|||
createPlugin("fake");
|
||||
Files.createFile(env.binFile().resolve("fake"));
|
||||
UserException e = expectThrows(UserException.class, () -> removePlugin("fake", home, randomBoolean()));
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("not a directory"));
|
||||
assertThat(e.getMessage(), containsString("not a directory"));
|
||||
assertTrue(Files.exists(env.pluginsFile().resolve("fake"))); // did not remove
|
||||
assertTrue(Files.exists(env.binFile().resolve("fake")));
|
||||
assertRemoveCleaned(env);
|
||||
|
@ -224,7 +209,7 @@ public class RemovePluginActionTests extends ESTestCase {
|
|||
|
||||
public void testPurgeNothingExists() throws Exception {
|
||||
final UserException e = expectThrows(UserException.class, () -> removePlugin("fake", home, true));
|
||||
assertThat(e, hasToString(containsString("plugin [fake] not found")));
|
||||
assertThat(e.getMessage(), containsString("plugin [fake] not found"));
|
||||
}
|
||||
|
||||
public void testPurgeOnlyMarkerFileExists() throws Exception {
|
||||
|
@ -276,7 +261,7 @@ public class RemovePluginActionTests extends ESTestCase {
|
|||
|
||||
e = expectThrows(UserException.class, () -> removePlugin(emptyList(), home, randomBoolean()));
|
||||
assertEquals(ExitCodes.USAGE, e.exitCode);
|
||||
assertEquals("At least one plugin ID is required", e.getMessage());
|
||||
assertThat(e.getMessage(), equalTo("At least one plugin ID is required"));
|
||||
}
|
||||
|
||||
public void testRemoveWhenRemovingMarker() throws Exception {
|
||||
|
|
|
@ -0,0 +1,296 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
package org.elasticsearch.plugins.cli;
|
||||
|
||||
import org.apache.lucene.util.LuceneTestCase;
|
||||
import org.elasticsearch.Version;
|
||||
import org.elasticsearch.cli.MockTerminal;
|
||||
import org.elasticsearch.cli.UserException;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.env.TestEnvironment;
|
||||
import org.elasticsearch.plugins.PluginTestUtil;
|
||||
import org.elasticsearch.plugins.cli.SyncPluginsAction.PluginChanges;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.hamcrest.Matchers.empty;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.mockito.ArgumentMatchers.anyList;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@LuceneTestCase.SuppressFileSystems("*")
|
||||
public class SyncPluginsActionTests extends ESTestCase {
|
||||
private Environment env;
|
||||
private SyncPluginsAction action;
|
||||
private PluginsConfig config;
|
||||
|
||||
@Override
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
Path home = createTempDir();
|
||||
Settings settings = Settings.builder().put("path.home", home).build();
|
||||
env = TestEnvironment.newEnvironment(settings);
|
||||
Files.createDirectories(env.binFile());
|
||||
Files.createFile(env.binFile().resolve("elasticsearch"));
|
||||
Files.createDirectories(env.configFile());
|
||||
Files.createDirectories(env.pluginsFile());
|
||||
|
||||
action = new SyncPluginsAction(new MockTerminal(), env);
|
||||
config = new PluginsConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that when we ensure a plugins config file doesn't exist, and it really doesn't exist,
|
||||
* then no exception is thrown.
|
||||
*/
|
||||
public void test_ensureNoConfigFile_withoutConfig_doesNothing() throws Exception {
|
||||
SyncPluginsAction.ensureNoConfigFile(env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that when we ensure a plugins config file doesn't exist, but a file does exist,
|
||||
* then an exception is thrown.
|
||||
*/
|
||||
public void test_ensureNoConfigFile_withConfig_throwsException() throws Exception {
|
||||
Files.createFile(env.configFile().resolve("elasticsearch-plugins.yml"));
|
||||
final UserException e = expectThrows(UserException.class, () -> SyncPluginsAction.ensureNoConfigFile(env));
|
||||
|
||||
assertThat(e.getMessage(), Matchers.matchesPattern("^Plugins config \\[.*] exists.*$"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that when there are no plugins to install, and no plugins already installed, then we
|
||||
* calculate that no changes are required.
|
||||
*/
|
||||
public void test_getPluginChanges_withNoChanges_returnsNoChanges() throws PluginSyncException {
|
||||
final SyncPluginsAction.PluginChanges pluginChanges = action.getPluginChanges(config, Optional.empty());
|
||||
|
||||
assertThat(pluginChanges.isEmpty(), is(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that when there are no plugins in the config file, and a plugin is already installed, then we
|
||||
* calculate that the plugin needs to be removed.
|
||||
*/
|
||||
public void test_getPluginChanges_withExtraPluginOnDisk_returnsPluginToRemove() throws Exception {
|
||||
createPlugin("my-plugin");
|
||||
|
||||
final PluginChanges pluginChanges = action.getPluginChanges(config, Optional.empty());
|
||||
|
||||
assertThat(pluginChanges.isEmpty(), is(false));
|
||||
assertThat(pluginChanges.install, empty());
|
||||
assertThat(pluginChanges.remove, hasSize(1));
|
||||
assertThat(pluginChanges.upgrade, empty());
|
||||
assertThat(pluginChanges.remove.get(0).getId(), equalTo("my-plugin"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that when there is a plugin in the config file, and no plugins already installed, then we
|
||||
* calculate that the plugin needs to be installed.
|
||||
*/
|
||||
public void test_getPluginChanges_withPluginToInstall_returnsPluginToInstall() throws Exception {
|
||||
config.setPlugins(List.of(new PluginDescriptor("my-plugin")));
|
||||
|
||||
final PluginChanges pluginChanges = action.getPluginChanges(config, Optional.empty());
|
||||
|
||||
assertThat(pluginChanges.isEmpty(), is(false));
|
||||
assertThat(pluginChanges.install, hasSize(1));
|
||||
assertThat(pluginChanges.remove, empty());
|
||||
assertThat(pluginChanges.upgrade, empty());
|
||||
assertThat(pluginChanges.install.get(0).getId(), equalTo("my-plugin"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that when there is an unofficial plugin in the config file, and that plugin is already installed
|
||||
* but needs to be upgraded due to the Elasticsearch version, then we calculate that no changes are required,
|
||||
* since we can't automatically upgrade it.
|
||||
*/
|
||||
public void test_getPluginChanges_withPluginToUpgrade_returnsNoChanges() throws Exception {
|
||||
createPlugin("my-plugin", Version.CURRENT.previousMajor());
|
||||
config.setPlugins(List.of(new PluginDescriptor("my-plugin")));
|
||||
|
||||
final PluginChanges pluginChanges = action.getPluginChanges(config, Optional.empty());
|
||||
|
||||
assertThat(pluginChanges.isEmpty(), is(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that when there is an official plugin in the config file, and that plugin is already installed
|
||||
* but needs to be upgraded, then we calculate that the plugin needs to be upgraded.
|
||||
*/
|
||||
public void test_getPluginChanges_withOfficialPluginToUpgrade_returnsPluginToUpgrade() throws Exception {
|
||||
createPlugin("analysis-icu", Version.CURRENT.previousMajor());
|
||||
config.setPlugins(List.of(new PluginDescriptor("analysis-icu")));
|
||||
|
||||
final PluginChanges pluginChanges = action.getPluginChanges(config, Optional.empty());
|
||||
|
||||
assertThat(pluginChanges.isEmpty(), is(false));
|
||||
assertThat(pluginChanges.install, empty());
|
||||
assertThat(pluginChanges.remove, empty());
|
||||
assertThat(pluginChanges.upgrade, hasSize(1));
|
||||
assertThat(pluginChanges.upgrade.get(0).getId(), equalTo("analysis-icu"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that if an unofficial plugins' location has not changed in the cached config, then we
|
||||
* calculate that the plugin does not need to be upgraded.
|
||||
*/
|
||||
public void test_getPluginChanges_withCachedConfigAndNoChanges_returnsNoChanges() throws Exception {
|
||||
createPlugin("my-plugin");
|
||||
config.setPlugins(List.of(new PluginDescriptor("my-plugin", "file://plugin.zip")));
|
||||
|
||||
final PluginsConfig cachedConfig = new PluginsConfig();
|
||||
cachedConfig.setPlugins(List.of(new PluginDescriptor("my-plugin", "file://plugin.zip")));
|
||||
|
||||
final PluginChanges pluginChanges = action.getPluginChanges(config, Optional.of(cachedConfig));
|
||||
|
||||
assertThat(pluginChanges.isEmpty(), is(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that if an unofficial plugins' location has changed, then we calculate that the plugin
|
||||
* needs to be upgraded.
|
||||
*/
|
||||
public void test_getPluginChanges_withCachedConfigAndChangedLocation_returnsPluginToUpgrade() throws Exception {
|
||||
createPlugin("my-plugin");
|
||||
config.setPlugins(List.of(new PluginDescriptor("my-plugin", "file:///after.zip")));
|
||||
|
||||
final PluginsConfig cachedConfig = new PluginsConfig();
|
||||
cachedConfig.setPlugins(List.of(new PluginDescriptor("my-plugin", "file://before.zip")));
|
||||
|
||||
final PluginChanges pluginChanges = action.getPluginChanges(config, Optional.of(cachedConfig));
|
||||
|
||||
assertThat(pluginChanges.isEmpty(), is(false));
|
||||
assertThat(pluginChanges.install, empty());
|
||||
assertThat(pluginChanges.remove, empty());
|
||||
assertThat(pluginChanges.upgrade, hasSize(1));
|
||||
assertThat(pluginChanges.upgrade.get(0).getId(), equalTo("my-plugin"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that if there are no changes to apply, then the install and remove actions are not used.
|
||||
* This is a redundant test, really, because the sync action exits early if there are no
|
||||
* changes.
|
||||
*/
|
||||
public void test_performSync_withNoChanges_doesNothing() throws Exception {
|
||||
final InstallPluginAction installAction = mock(InstallPluginAction.class);
|
||||
final RemovePluginAction removeAction = mock(RemovePluginAction.class);
|
||||
|
||||
action.performSync(installAction, removeAction, new PluginChanges(List.of(), List.of(), List.of()));
|
||||
|
||||
verify(installAction, never()).execute(anyList());
|
||||
verify(removeAction, never()).execute(anyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that if there are plugins to remove, then the remove action is used.
|
||||
*/
|
||||
public void test_performSync_withPluginsToRemove_callsRemoveAction() throws Exception {
|
||||
final InstallPluginAction installAction = mock(InstallPluginAction.class);
|
||||
final RemovePluginAction removeAction = mock(RemovePluginAction.class);
|
||||
final List<PluginDescriptor> pluginDescriptors = List.of(new PluginDescriptor("plugin1"), new PluginDescriptor("plugin2"));
|
||||
|
||||
action.performSync(installAction, removeAction, new PluginChanges(pluginDescriptors, List.of(), List.of()));
|
||||
|
||||
verify(installAction, never()).execute(anyList());
|
||||
verify(removeAction).setPurge(true);
|
||||
verify(removeAction).execute(pluginDescriptors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that if there are plugins to install, then the install action is used.
|
||||
*/
|
||||
public void test_performSync_withPluginsToInstall_callsInstallAction() throws Exception {
|
||||
final InstallPluginAction installAction = mock(InstallPluginAction.class);
|
||||
final RemovePluginAction removeAction = mock(RemovePluginAction.class);
|
||||
final List<PluginDescriptor> pluginDescriptors = List.of(new PluginDescriptor("plugin1"), new PluginDescriptor("plugin2"));
|
||||
|
||||
action.performSync(installAction, removeAction, new PluginChanges(List.of(), pluginDescriptors, List.of()));
|
||||
|
||||
verify(installAction).execute(pluginDescriptors);
|
||||
verify(removeAction, never()).execute(anyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that if there are plugins to upgrade, then both the install and remove actions are used.
|
||||
*/
|
||||
public void test_performSync_withPluginsToUpgrade_callsRemoveAndInstallAction() throws Exception {
|
||||
final InstallPluginAction installAction = mock(InstallPluginAction.class);
|
||||
final RemovePluginAction removeAction = mock(RemovePluginAction.class);
|
||||
final InOrder inOrder = Mockito.inOrder(removeAction, installAction);
|
||||
|
||||
final List<PluginDescriptor> pluginDescriptors = List.of(new PluginDescriptor("plugin1"), new PluginDescriptor("plugin2"));
|
||||
|
||||
action.performSync(installAction, removeAction, new PluginChanges(List.of(), List.of(), pluginDescriptors));
|
||||
|
||||
inOrder.verify(removeAction).setPurge(false);
|
||||
inOrder.verify(removeAction).execute(pluginDescriptors);
|
||||
inOrder.verify(installAction).execute(pluginDescriptors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that if there are plugins to remove, install and upgrade, then we do everything.
|
||||
*/
|
||||
public void test_performSync_withPluginsToUpgrade_callsUpgradeAction() throws Exception {
|
||||
final InstallPluginAction installAction = mock(InstallPluginAction.class);
|
||||
final RemovePluginAction removeAction = mock(RemovePluginAction.class);
|
||||
final InOrder inOrder = Mockito.inOrder(removeAction, installAction);
|
||||
|
||||
final List<PluginDescriptor> pluginsToRemove = List.of(new PluginDescriptor("plugin1"));
|
||||
final List<PluginDescriptor> pluginsToInstall = List.of(new PluginDescriptor("plugin2"));
|
||||
final List<PluginDescriptor> pluginsToUpgrade = List.of(new PluginDescriptor("plugin3"));
|
||||
|
||||
action.performSync(installAction, removeAction, new PluginChanges(pluginsToRemove, pluginsToInstall, pluginsToUpgrade));
|
||||
|
||||
inOrder.verify(removeAction).setPurge(true);
|
||||
inOrder.verify(removeAction).execute(pluginsToRemove);
|
||||
|
||||
inOrder.verify(installAction).execute(pluginsToInstall);
|
||||
|
||||
inOrder.verify(removeAction).setPurge(false);
|
||||
inOrder.verify(removeAction).execute(pluginsToUpgrade);
|
||||
inOrder.verify(installAction).execute(pluginsToUpgrade);
|
||||
}
|
||||
|
||||
private void createPlugin(String name) throws IOException {
|
||||
createPlugin(name, Version.CURRENT);
|
||||
}
|
||||
|
||||
private void createPlugin(String name, Version version) throws IOException {
|
||||
PluginTestUtil.writePluginProperties(
|
||||
env.pluginsFile().resolve(name),
|
||||
"description",
|
||||
"dummy",
|
||||
"name",
|
||||
name,
|
||||
"version",
|
||||
"1.0",
|
||||
"elasticsearch.version",
|
||||
version.toString(),
|
||||
"java.version",
|
||||
System.getProperty("java.specification.version"),
|
||||
"classname",
|
||||
"SomeClass"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@ import java.util.Locale;
|
|||
* The available methods are similar to those of {@link Console}, with the ability
|
||||
* to read either normal text or a password, and the ability to print a line
|
||||
* of text. Printing is also gated by the {@link Verbosity} of the terminal,
|
||||
* which allows {@link #println(Verbosity,String)} calls which act like a logger,
|
||||
* which allows {@link #println(Verbosity,CharSequence)} calls which act like a logger,
|
||||
* only actually printing if the verbosity level of the terminal is above
|
||||
* the verbosity of the message.
|
||||
*/
|
||||
|
@ -113,7 +113,7 @@ public abstract class Terminal {
|
|||
}
|
||||
|
||||
/** Prints message to the terminal at {@code verbosity} level, without a newline. */
|
||||
private void print(Verbosity verbosity, String msg, boolean isError) {
|
||||
protected void print(Verbosity verbosity, String msg, boolean isError) {
|
||||
if (isPrintable(verbosity)) {
|
||||
PrintWriter writer = isError ? getErrorWriter() : getWriter();
|
||||
writer.print(msg);
|
||||
|
@ -206,6 +206,16 @@ public abstract class Terminal {
|
|||
this.getErrorWriter().flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this terminal is for a headless system i.e. is not interactive. If an instances answers
|
||||
* {@code false}, interactive operations can be attempted, but it is not guaranteed that they will succeed.
|
||||
*
|
||||
* @return if this terminal is headless.
|
||||
*/
|
||||
public boolean isHeadless() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class ConsoleTerminal extends Terminal {
|
||||
|
||||
private static final Console CONSOLE = System.console();
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
package org.elasticsearch.packaging.test;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
|
@ -19,6 +20,7 @@ import org.elasticsearch.packaging.util.ServerUtils;
|
|||
import org.elasticsearch.packaging.util.Shell;
|
||||
import org.elasticsearch.packaging.util.Shell.Result;
|
||||
import org.elasticsearch.packaging.util.docker.DockerRun;
|
||||
import org.elasticsearch.packaging.util.docker.MockServer;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
|
@ -32,6 +34,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
@ -46,6 +49,7 @@ import static org.elasticsearch.packaging.util.FileMatcher.p750;
|
|||
import static org.elasticsearch.packaging.util.FileMatcher.p755;
|
||||
import static org.elasticsearch.packaging.util.FileMatcher.p775;
|
||||
import static org.elasticsearch.packaging.util.FileUtils.append;
|
||||
import static org.elasticsearch.packaging.util.FileUtils.deleteIfExists;
|
||||
import static org.elasticsearch.packaging.util.FileUtils.rm;
|
||||
import static org.elasticsearch.packaging.util.docker.Docker.chownWithPrivilegeEscalation;
|
||||
import static org.elasticsearch.packaging.util.docker.Docker.copyFromContainer;
|
||||
|
@ -69,11 +73,14 @@ import static org.elasticsearch.packaging.util.docker.DockerRun.builder;
|
|||
import static org.hamcrest.Matchers.allOf;
|
||||
import static org.hamcrest.Matchers.arrayContaining;
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.emptyString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.hasEntry;
|
||||
import static org.hamcrest.Matchers.hasItems;
|
||||
import static org.hamcrest.Matchers.hasKey;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.matchesPattern;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
|
@ -92,10 +99,14 @@ import static org.junit.Assume.assumeTrue;
|
|||
* <li>Images for Cloud</li>
|
||||
* </ul>
|
||||
*/
|
||||
@ThreadLeakFilters(defaultFilters = true, filters = { HttpClientThreadsFilter.class })
|
||||
public class DockerTests extends PackagingTestCase {
|
||||
private Path tempDir;
|
||||
private static final String PASSWORD = "nothunter2";
|
||||
|
||||
private static final String EXAMPLE_PLUGIN_SYSPROP = "tests.example-plugin";
|
||||
private static final String EXAMPLE_PLUGIN_PATH = System.getProperty(EXAMPLE_PLUGIN_SYSPROP);
|
||||
|
||||
@BeforeClass
|
||||
public static void filterDistros() {
|
||||
assumeTrue("only Docker", distribution().isDocker());
|
||||
|
@ -159,7 +170,7 @@ public class DockerTests extends PackagingTestCase {
|
|||
/**
|
||||
* Check that Cloud images bundle a selection of plugins.
|
||||
*/
|
||||
public void test021PluginsListWithPlugins() {
|
||||
public void test021PluginsListWithDefaultCloudPlugins() {
|
||||
assumeTrue(
|
||||
"Only applies to Cloud images",
|
||||
distribution.packaging == Packaging.DOCKER_CLOUD || distribution().packaging == Packaging.DOCKER_CLOUD_ESS
|
||||
|
@ -176,30 +187,187 @@ public class DockerTests extends PackagingTestCase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks that ESS images can install plugins from the local archive.
|
||||
* Check that a plugin can be installed without special permissions.
|
||||
*/
|
||||
public void test022InstallPluginsFromLocalArchive() {
|
||||
assumeTrue("Only applies to ESS images", distribution().packaging == Packaging.DOCKER_CLOUD_ESS);
|
||||
public void test022InstallPlugin() {
|
||||
runContainer(
|
||||
distribution(),
|
||||
builder().envVar("ELASTIC_PASSWORD", PASSWORD).volume(Path.of(EXAMPLE_PLUGIN_PATH), "/analysis-icu.zip")
|
||||
);
|
||||
|
||||
final String plugin = "analysis-icu";
|
||||
assertThat("Expected " + plugin + " to not be installed", listPlugins(), not(hasItems(plugin)));
|
||||
|
||||
final Installation.Executables bin = installation.executables();
|
||||
List<String> plugins = sh.run(bin.pluginTool + " list").stdout.lines().collect(Collectors.toList());
|
||||
sh.run(bin.pluginTool + " install file:///analysis-icu.zip");
|
||||
|
||||
assertThat("Expected " + plugin + " to not be installed", plugins, not(hasItems(plugin)));
|
||||
final boolean isCloudImage = distribution().packaging == Packaging.DOCKER_CLOUD
|
||||
|| distribution().packaging == Packaging.DOCKER_CLOUD_ESS;
|
||||
|
||||
final List<String> expectedPlugins = isCloudImage
|
||||
? List.of("repository-azure", "repository-gcs", "repository-s3", "analysis-icu")
|
||||
: List.of("analysis-icu");
|
||||
|
||||
assertThat("Expected installed plugins to be listed", listPlugins(), equalTo(expectedPlugins));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that ESS images can install plugins from the local archive.
|
||||
*/
|
||||
public void test023InstallPluginsFromLocalArchive() {
|
||||
assumeTrue("Only ESS images have a local archive", distribution().packaging == Packaging.DOCKER_CLOUD_ESS);
|
||||
|
||||
final String plugin = "analysis-icu";
|
||||
final Installation.Executables bin = installation.executables();
|
||||
|
||||
assertThat("Expected " + plugin + " to not be installed", listPlugins(), not(hasItems(plugin)));
|
||||
|
||||
// Stuff the proxy settings with garbage, so any attempt to go out to the internet would fail
|
||||
sh.getEnv()
|
||||
.put("ES_JAVA_OPTS", "-Dhttp.proxyHost=example.org -Dhttp.proxyPort=9999 -Dhttps.proxyHost=example.org -Dhttps.proxyPort=9999");
|
||||
sh.run("/opt/plugins/plugin-wrapper.sh install --batch analysis-icu");
|
||||
sh.run(bin.pluginTool + " install --batch analysis-icu");
|
||||
|
||||
plugins = sh.run(bin.pluginTool + " list").stdout.lines().collect(Collectors.toList());
|
||||
|
||||
assertThat("Expected " + plugin + " to be installed", plugins, hasItems(plugin));
|
||||
assertThat("Expected " + plugin + " to be installed", listPlugins(), hasItems(plugin));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the JDK's cacerts file is a symlink to the copy provided by the operating system.
|
||||
* Checks that plugins can be installed by deploying a plugins config file.
|
||||
*/
|
||||
public void test024InstallPluginUsingConfigFile() {
|
||||
final boolean isCloudImage = distribution().packaging == Packaging.DOCKER_CLOUD
|
||||
|| distribution().packaging == Packaging.DOCKER_CLOUD_ESS;
|
||||
|
||||
final StringJoiner pluginsDescriptor = new StringJoiner("\n", "", "\n");
|
||||
pluginsDescriptor.add("plugins:");
|
||||
pluginsDescriptor.add(" - id: analysis-icu");
|
||||
pluginsDescriptor.add(" location: file:///analysis-icu.zip");
|
||||
if (isCloudImage) {
|
||||
// The repository plugins have to be present, because (1) they are preinstalled, and (2) they
|
||||
// are owned by `root` and can't be removed.
|
||||
Stream.of("repository-s3", "repository-azure", "repository-gcs").forEach(plugin -> pluginsDescriptor.add(" - id: " + plugin));
|
||||
}
|
||||
|
||||
final String filename = "elasticsearch-plugins.yml";
|
||||
append(tempDir.resolve(filename), pluginsDescriptor.toString());
|
||||
|
||||
// Restart the container. This will sync the plugins automatically. Also
|
||||
// stuff the proxy settings with garbage, so any attempt to go out to the internet would fail. The
|
||||
// command should instead use the bundled plugin archive.
|
||||
runContainer(
|
||||
distribution(),
|
||||
builder().volume(tempDir.resolve(filename), installation.config.resolve(filename))
|
||||
.volume(Path.of(EXAMPLE_PLUGIN_PATH), "/analysis-icu.zip")
|
||||
.envVar("ELASTIC_PASSWORD", PASSWORD)
|
||||
.envVar(
|
||||
"ES_JAVA_OPTS",
|
||||
"-Dhttp.proxyHost=example.org -Dhttp.proxyPort=9999 -Dhttps.proxyHost=example.org -Dhttps.proxyPort=9999"
|
||||
)
|
||||
);
|
||||
|
||||
// Since ES is doing the installing, give it a chance to complete
|
||||
waitForElasticsearch(installation, "elastic", PASSWORD);
|
||||
|
||||
assertThat("List of installed plugins is incorrect", listPlugins(), hasItems("analysis-icu"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that ESS images can manage plugins from the local archive by deploying a plugins config file.
|
||||
*/
|
||||
public void test025InstallPluginFromArchiveUsingConfigFile() {
|
||||
assumeTrue("Only ESS image has a plugin archive", distribution().packaging == Packaging.DOCKER_CLOUD_ESS);
|
||||
|
||||
// The repository plugins have to be present, because (1) they are preinstalled, and (2) they
|
||||
// are owned by `root` and can't be removed.
|
||||
final String[] plugins = { "repository-s3", "repository-azure", "repository-gcs", "analysis-icu", "analysis-phonetic" };
|
||||
|
||||
final StringJoiner pluginsDescriptor = new StringJoiner("\n", "", "\n");
|
||||
pluginsDescriptor.add("plugins:");
|
||||
for (String plugin : plugins) {
|
||||
pluginsDescriptor.add(" - id: " + plugin);
|
||||
}
|
||||
|
||||
final String filename = "elasticsearch-plugins.yml";
|
||||
append(tempDir.resolve(filename), pluginsDescriptor.toString());
|
||||
|
||||
// Restart the container. This will sync the plugins automatically. Also
|
||||
// stuff the proxy settings with garbage, so any attempt to go out to the internet would fail. The
|
||||
// command should instead use the bundled plugin archive.
|
||||
runContainer(
|
||||
distribution(),
|
||||
builder().volume(tempDir.resolve(filename), installation.config.resolve(filename))
|
||||
.envVar("ELASTIC_PASSWORD", PASSWORD)
|
||||
.envVar(
|
||||
"ES_JAVA_OPTS",
|
||||
"-Dhttp.proxyHost=example.org -Dhttp.proxyPort=9999 -Dhttps.proxyHost=example.org -Dhttps.proxyPort=9999"
|
||||
)
|
||||
);
|
||||
|
||||
// Since ES is doing the installing, give it a chance to complete
|
||||
waitForElasticsearch(installation, "elastic", PASSWORD);
|
||||
|
||||
assertThat("List of installed plugins is incorrect", listPlugins(), containsInAnyOrder(plugins));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that when using Elasticsearch's plugins sync capability, it will use a proxy when configured to do so.
|
||||
* This could either be in the plugins config file, or via the standard Java system properties.
|
||||
*/
|
||||
public void test024SyncPluginsUsingProxy() {
|
||||
MockServer.withMockServer(mockServer -> {
|
||||
for (boolean useConfigFile : List.of(true, false)) {
|
||||
mockServer.clearExpectations();
|
||||
|
||||
final StringJoiner config = new StringJoiner("\n", "", "\n");
|
||||
config.add("plugins:");
|
||||
// The repository plugins have to be present for Cloud images, because (1) they are preinstalled, and (2) they
|
||||
// are owned by `root` and can't be removed.
|
||||
if (distribution().packaging == Packaging.DOCKER_CLOUD || distribution().packaging == Packaging.DOCKER_CLOUD_ESS) {
|
||||
for (String plugin : List.of("repository-s3", "repository-azure", "repository-gcs", "analysis-icu")) {
|
||||
config.add(" - id: " + plugin);
|
||||
}
|
||||
}
|
||||
// This is the new plugin to install. We don't use an official plugin because then Elasticsearch
|
||||
// will attempt an SSL connection and that just makes everything more complicated.
|
||||
config.add(" - id: my-plugin");
|
||||
config.add(" location: http://example.com/my-plugin.zip");
|
||||
|
||||
if (useConfigFile) {
|
||||
config.add("proxy: mockserver:" + mockServer.getPort());
|
||||
}
|
||||
|
||||
final String filename = "elasticsearch-plugins.yml";
|
||||
final Path pluginsConfigPath = tempDir.resolve(filename);
|
||||
deleteIfExists(pluginsConfigPath);
|
||||
append(pluginsConfigPath, config.toString());
|
||||
|
||||
final DockerRun builder = builder().volume(pluginsConfigPath, installation.config.resolve(filename))
|
||||
.extraArgs("--link " + mockServer.getContainerId() + ":mockserver");
|
||||
|
||||
if (useConfigFile == false) {
|
||||
builder.envVar("ES_JAVA_OPTS", "-Dhttp.proxyHost=mockserver -Dhttp.proxyPort=" + mockServer.getPort());
|
||||
}
|
||||
|
||||
// Restart the container. This will sync plugins automatically, which will fail because
|
||||
// ES will be unable to install `my-plugin`
|
||||
final Result result = runContainerExpectingFailure(distribution(), builder);
|
||||
|
||||
final List<Map<String, String>> interactions = mockServer.getInteractions();
|
||||
|
||||
assertThat(result.stderr, containsString("FileNotFoundException: http://example.com/my-plugin.zip"));
|
||||
|
||||
// Now check that Elasticsearch did use the proxy server
|
||||
assertThat(interactions, hasSize(1));
|
||||
final Map<String, String> interaction = interactions.get(0);
|
||||
assertThat(interaction, hasEntry("httpRequest.headers.Host[0]", "example.com"));
|
||||
assertThat(interaction, hasEntry("httpRequest.headers.User-Agent[0]", "elasticsearch-plugin-installer"));
|
||||
assertThat(interaction, hasEntry("httpRequest.method", "GET"));
|
||||
assertThat(interaction, hasEntry("httpRequest.path", "/my-plugin.zip"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the JDK's `cacerts` file is a symlink to the copy provided by the operating system.
|
||||
*/
|
||||
public void test040JavaUsesTheOsProvidedKeystore() {
|
||||
final String path = sh.run("realpath jdk/lib/security/cacerts").stdout;
|
||||
|
@ -1048,4 +1216,9 @@ public class DockerTests extends PackagingTestCase {
|
|||
assertThat(Path.of("/opt/" + beat + "/modules.d"), file(Directory, "root", "root", p755));
|
||||
});
|
||||
}
|
||||
|
||||
private List<String> listPlugins() {
|
||||
final Installation.Executables bin = installation.executables();
|
||||
return sh.run(bin.pluginTool + " list").stdout.lines().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.packaging.test;
|
||||
|
||||
import com.carrotsearch.randomizedtesting.ThreadFilter;
|
||||
|
||||
/**
|
||||
* Java's {@link java.net.http.HttpClient} spawns threads, which causes our thread leak
|
||||
* detection to fail. Filter these threads out since AFAICT we can't completely clean them up.
|
||||
*/
|
||||
public class HttpClientThreadsFilter implements ThreadFilter {
|
||||
@Override
|
||||
public boolean reject(Thread t) {
|
||||
return t.getName().startsWith("HttpClient");
|
||||
}
|
||||
}
|
|
@ -8,22 +8,25 @@
|
|||
|
||||
package org.elasticsearch.packaging.test;
|
||||
|
||||
import org.elasticsearch.packaging.test.PackagingTestCase.AwaitsFix;
|
||||
import org.elasticsearch.packaging.util.FileUtils;
|
||||
import org.elasticsearch.packaging.util.Installation;
|
||||
import org.elasticsearch.packaging.util.Platforms;
|
||||
import org.elasticsearch.packaging.util.Shell;
|
||||
import org.junit.Before;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.matchesPattern;
|
||||
import static org.junit.Assume.assumeFalse;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
@AwaitsFix(bugUrl = "Needs to be re-enabled")
|
||||
@PackagingTestCase.AwaitsFix(bugUrl = "Needs to be re-enabled")
|
||||
public class PluginCliTests extends PackagingTestCase {
|
||||
|
||||
private static final String EXAMPLE_PLUGIN_NAME = "custom-settings";
|
||||
|
@ -31,7 +34,7 @@ public class PluginCliTests extends PackagingTestCase {
|
|||
|
||||
static {
|
||||
// re-read before each test so the plugin path can be manipulated within tests
|
||||
EXAMPLE_PLUGIN_ZIP = Paths.get(System.getProperty("tests.example-plugin", "/dummy/path"));
|
||||
EXAMPLE_PLUGIN_ZIP = Paths.get(System.getProperty("tests.example-plugin"));
|
||||
}
|
||||
|
||||
@Before
|
||||
|
@ -46,7 +49,7 @@ public class PluginCliTests extends PackagingTestCase {
|
|||
|
||||
private Shell.Result assertWithPlugin(Installation.Executable pluginTool, Path pluginZip, String pluginName, PluginAction action)
|
||||
throws Exception {
|
||||
Shell.Result installResult = pluginTool.run("install --batch \"" + pluginZip.toUri().toString() + "\"");
|
||||
Shell.Result installResult = pluginTool.run("install --batch \"" + pluginZip.toUri() + "\"");
|
||||
action.run(installResult);
|
||||
return pluginTool.run("remove " + pluginName);
|
||||
}
|
||||
|
@ -70,15 +73,13 @@ public class PluginCliTests extends PackagingTestCase {
|
|||
Platforms.onLinux(() -> sh.run("chown elasticsearch:elasticsearch " + linkedPlugins.toString()));
|
||||
Files.createSymbolicLink(pluginsDir, linkedPlugins);
|
||||
// Packaged installation don't get autoconfigured yet
|
||||
// TODO: Remove this in https://github.com/elastic/elasticsearch/pull/75144
|
||||
String protocol = distribution.isPackage() ? "http" : "https";
|
||||
assertWithExamplePlugin(installResult -> {
|
||||
assertWhileRunning(() -> {
|
||||
final String pluginsResponse = makeRequest(protocol + "://localhost:9200/_cat/plugins?h=component").strip();
|
||||
final String pluginsResponse = makeRequest("https://localhost:9200/_cat/plugins?h=component").strip();
|
||||
assertThat(pluginsResponse, equalTo(EXAMPLE_PLUGIN_NAME));
|
||||
|
||||
String settingsPath = "_cluster/settings?include_defaults&filter_path=defaults.custom.simple";
|
||||
final String settingsResponse = makeRequest(protocol + "://localhost:9200/" + settingsPath).strip();
|
||||
final String settingsResponse = makeRequest("https://localhost:9200/" + settingsPath).strip();
|
||||
assertThat(settingsResponse, equalTo("{\"defaults\":{\"custom\":{\"simple\":\"foo\"}}}"));
|
||||
});
|
||||
});
|
||||
|
@ -119,4 +120,44 @@ public class PluginCliTests extends PackagingTestCase {
|
|||
sh.setUmask("0077");
|
||||
assertWithExamplePlugin(installResult -> {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the `install` subcommand cannot be used if a plugins config file exists.
|
||||
*/
|
||||
public void test30InstallFailsIfConfigFilePresent() throws IOException {
|
||||
Files.writeString(installation.config.resolve("elasticsearch-plugins.yml"), "");
|
||||
|
||||
Shell.Result result = installation.executables().pluginTool.run("install analysis-icu", null, true);
|
||||
assertThat(result.isSuccess(), is(false));
|
||||
assertThat(result.stderr, matchesPattern("^Plugins config \\[[^+]] exists.*"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the `remove` subcommand cannot be used if a plugins config file exists.
|
||||
*/
|
||||
public void test31RemoveFailsIfConfigFilePresent() throws IOException {
|
||||
Files.writeString(installation.config.resolve("elasticsearch-plugins.yml"), "");
|
||||
|
||||
Shell.Result result = installation.executables().pluginTool.run("install analysis-icu", null, true);
|
||||
assertThat(result.isSuccess(), is(false));
|
||||
assertThat(result.stderr, matchesPattern("^Plugins config \\[[^+]] exists.*"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that when a plugins config file exists, Elasticsearch refuses to start up, since using
|
||||
* a config file is only supported in Docker.
|
||||
*/
|
||||
public void test32FailsToStartWhenPluginsConfigExists() throws Exception {
|
||||
try {
|
||||
Files.writeString(installation.config("elasticsearch-plugins.yml"), "content doesn't matter for this test");
|
||||
Shell.Result result = runElasticsearchStartCommand(null, false, true);
|
||||
assertThat(result.isSuccess(), equalTo(false));
|
||||
assertThat(
|
||||
result.stderr,
|
||||
containsString("Can only use [elasticsearch-plugins.yml] config file with distribution type [docker]")
|
||||
);
|
||||
} finally {
|
||||
FileUtils.rm(installation.config("elasticsearch-plugins.yml"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -195,7 +195,7 @@ public class Docker {
|
|||
do {
|
||||
try {
|
||||
// Give the container a chance to exit out
|
||||
Thread.sleep(1000);
|
||||
Thread.sleep(2000);
|
||||
|
||||
if (sh.run("docker ps --quiet --no-trunc").stdout.contains(containerId) == false) {
|
||||
isElasticsearchRunning = false;
|
||||
|
@ -462,8 +462,6 @@ public class Docker {
|
|||
}
|
||||
|
||||
private static void verifyCloudContainerInstallation(Installation es) {
|
||||
assertThat(Path.of("/opt/plugins/plugin-wrapper.sh"), file("root", "root", p555));
|
||||
|
||||
final String pluginArchive = "/opt/plugins/archive";
|
||||
final List<String> plugins = listContents(pluginArchive);
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ package org.elasticsearch.packaging.util.docker;
|
|||
import org.elasticsearch.packaging.util.Distribution;
|
||||
import org.elasticsearch.packaging.util.Platforms;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
@ -53,6 +54,10 @@ public class DockerRun {
|
|||
}
|
||||
|
||||
public DockerRun volume(Path from, String to) {
|
||||
requireNonNull(from);
|
||||
if (Files.exists(from) == false) {
|
||||
throw new RuntimeException("Path [" + from + "] does not exist");
|
||||
}
|
||||
this.volumes.put(requireNonNull(from), Path.of(requireNonNull(to)));
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.packaging.util.docker;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.ValueNode;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.elasticsearch.core.CheckedConsumer;
|
||||
import org.elasticsearch.packaging.test.PackagingTestCase;
|
||||
import org.elasticsearch.packaging.util.Shell;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
/**
|
||||
* Providers an interface to <a href="https://org.mock-server.com/">Mockserver</a>, where a proxy
|
||||
* server is needed for testing in Docker tests.
|
||||
* <p>
|
||||
* To use the server, link the container under test with the mockserver using the <code>--link</code>
|
||||
* CLI option, using the {@link #getContainerId()} option. By aliasing the ID, you will know what
|
||||
* hostname to use to connect to the proxy. For example:
|
||||
*
|
||||
* <pre>"--link " + mockserver.getContainerId() + ":mockserver"</pre>
|
||||
*
|
||||
* <p>All requests will result in a 404, but those requests are recorded and can be retried with
|
||||
* {@link #getInteractions()}. These can can be reset with {@link #clearExpectations()}.
|
||||
*/
|
||||
public class MockServer {
|
||||
protected final Logger logger = LogManager.getLogger(getClass());
|
||||
|
||||
private static final int CONTAINER_PORT = 1080; // default for image
|
||||
|
||||
private final Shell shell;
|
||||
private final HttpClient client;
|
||||
private ExecutorService executorService;
|
||||
private String containerId;
|
||||
|
||||
/**
|
||||
* Create a new mockserver, and execute the supplied {@code runnable}. The mockserver will
|
||||
* be cleaned up afterwards.
|
||||
* @param runnable the code to run e.g. the test case
|
||||
*/
|
||||
public static void withMockServer(CheckedConsumer<MockServer, Exception> runnable) {
|
||||
final MockServer mockServer = new MockServer();
|
||||
try {
|
||||
mockServer.start();
|
||||
runnable.accept(mockServer);
|
||||
mockServer.close();
|
||||
} catch (Throwable e) {
|
||||
mockServer.close();
|
||||
}
|
||||
}
|
||||
|
||||
private MockServer() {
|
||||
this.shell = new Shell();
|
||||
this.executorService = Executors.newSingleThreadExecutor();
|
||||
this.client = HttpClient.newBuilder().executor(executorService).build();
|
||||
}
|
||||
|
||||
private void start() throws Exception {
|
||||
final String command = "docker run -t --detach --rm -p " + CONTAINER_PORT + ":" + CONTAINER_PORT + " mockserver/mockserver:latest";
|
||||
this.containerId = this.shell.run(command).stdout.trim();
|
||||
|
||||
// It's a Java app, so give it a chance to wake up. I'd add a healthcheck to the above command,
|
||||
// but the image doesn't have any CLI utils at all.
|
||||
PackagingTestCase.assertBusy(() -> {
|
||||
try {
|
||||
this.reset();
|
||||
} catch (Exception e) {
|
||||
// Only assertions are retried.
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}, 20, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void clearExpectations() throws Exception {
|
||||
doRequest("http://localhost:" + CONTAINER_PORT + "/mockserver/clear?type=EXPECTATIONS", "{ \"path\": \"/*\" }");
|
||||
}
|
||||
|
||||
public void reset() throws Exception {
|
||||
doRequest("http://localhost:" + CONTAINER_PORT + "/mockserver/reset", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all interactions with the mockserver since startup, the last call to {@link #reset()} or the
|
||||
* last call to {@link #clearExpectations()}. The JSON returned by the mockserver is flattened, so that
|
||||
* the period-seperated keys in each map represent the structure of the JSON.
|
||||
*
|
||||
* @return a list of interactions
|
||||
* @throws Exception if anything goes wrong
|
||||
*/
|
||||
public List<Map<String, String>> getInteractions() throws Exception {
|
||||
final String url = "http://localhost:" + CONTAINER_PORT + "/mockserver/retrieve?type=REQUEST_RESPONSES";
|
||||
|
||||
final String result = doRequest(url, null);
|
||||
|
||||
final ObjectMapper objectMapper = new ObjectMapper();
|
||||
final JsonNode jsonNode = objectMapper.readTree(result);
|
||||
|
||||
assertThat("Response from mockserver is not a JSON array", jsonNode.isArray(), is(true));
|
||||
|
||||
final List<Map<String, String>> interactions = new ArrayList<>();
|
||||
|
||||
for (JsonNode node : jsonNode) {
|
||||
final Map<String, String> interaction = new HashMap<>();
|
||||
addKeys("", node, interaction);
|
||||
interactions.add(interaction);
|
||||
}
|
||||
|
||||
return interactions;
|
||||
}
|
||||
|
||||
private void close() {
|
||||
if (this.containerId != null) {
|
||||
this.shell.run("docker rm -f " + this.containerId);
|
||||
this.containerId = null;
|
||||
}
|
||||
|
||||
if (this.executorService != null) {
|
||||
this.executorService.shutdown();
|
||||
this.executorService = null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getContainerId() {
|
||||
return containerId;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return CONTAINER_PORT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively flattens a JsonNode into a map, to make it easier to pick out entries and make assertions.
|
||||
* Keys are concatenated with periods.
|
||||
*
|
||||
* @param currentPath used recursively to construct the key
|
||||
* @param jsonNode the current node to flatten
|
||||
* @param map entries are added into this map
|
||||
*/
|
||||
private void addKeys(String currentPath, JsonNode jsonNode, Map<String, String> map) {
|
||||
if (jsonNode.isObject()) {
|
||||
ObjectNode objectNode = (ObjectNode) jsonNode;
|
||||
Iterator<Map.Entry<String, JsonNode>> iter = objectNode.fields();
|
||||
String pathPrefix = currentPath.isEmpty() ? "" : currentPath + ".";
|
||||
|
||||
while (iter.hasNext()) {
|
||||
Map.Entry<String, JsonNode> entry = iter.next();
|
||||
addKeys(pathPrefix + entry.getKey(), entry.getValue(), map);
|
||||
}
|
||||
} else if (jsonNode.isArray()) {
|
||||
ArrayNode arrayNode = (ArrayNode) jsonNode;
|
||||
for (int i = 0; i < arrayNode.size(); i++) {
|
||||
addKeys(currentPath + "[" + i + "]", arrayNode.get(i), map);
|
||||
}
|
||||
} else if (jsonNode.isValueNode()) {
|
||||
ValueNode valueNode = (ValueNode) jsonNode;
|
||||
map.put(currentPath, valueNode.asText());
|
||||
}
|
||||
}
|
||||
|
||||
private String doRequest(String urlString, String body) throws Exception {
|
||||
final HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(urlString));
|
||||
|
||||
if (body == null) {
|
||||
request.method("PUT", BodyPublishers.noBody());
|
||||
} else {
|
||||
request.method("PUT", BodyPublishers.ofString(body)).header("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
final HttpResponse<String> response = client.send(request.build(), BodyHandlers.ofString());
|
||||
|
||||
return response.body();
|
||||
}
|
||||
}
|
|
@ -16,8 +16,10 @@ import org.apache.logging.log4j.core.appender.ConsoleAppender;
|
|||
import org.apache.logging.log4j.core.config.Configurator;
|
||||
import org.apache.lucene.util.Constants;
|
||||
import org.apache.lucene.util.StringHelper;
|
||||
import org.elasticsearch.Build;
|
||||
import org.elasticsearch.ElasticsearchException;
|
||||
import org.elasticsearch.Version;
|
||||
import org.elasticsearch.bootstrap.plugins.PluginsManager;
|
||||
import org.elasticsearch.cli.UserException;
|
||||
import org.elasticsearch.common.PidFile;
|
||||
import org.elasticsearch.common.filesystem.FileSystemNatives;
|
||||
|
@ -339,6 +341,20 @@ final class Bootstrap {
|
|||
// setDefaultUncaughtExceptionHandler
|
||||
Thread.setDefaultUncaughtExceptionHandler(new ElasticsearchUncaughtExceptionHandler());
|
||||
|
||||
if (PluginsManager.configExists(environment)) {
|
||||
if (Build.CURRENT.type() == Build.Type.DOCKER) {
|
||||
try {
|
||||
PluginsManager.syncPlugins(environment);
|
||||
} catch (Exception e) {
|
||||
throw new BootstrapException(e);
|
||||
}
|
||||
} else {
|
||||
throw new BootstrapException(
|
||||
new ElasticsearchException("Can only use [elasticsearch-plugins.yml] config file with distribution type [docker]")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
INSTANCE.setup(true, environment);
|
||||
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.bootstrap.plugins;
|
||||
|
||||
import org.apache.logging.log4j.Level;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.spi.AbstractLogger;
|
||||
import org.apache.logging.log4j.spi.ExtendedLoggerWrapper;
|
||||
import org.elasticsearch.cli.Terminal;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
public final class LoggerTerminal extends Terminal {
|
||||
private final ExtendedLoggerWrapper logger;
|
||||
|
||||
private static final String FQCN = LoggerTerminal.class.getName();
|
||||
|
||||
private LoggerTerminal(final Logger logger) {
|
||||
super(System.lineSeparator());
|
||||
this.logger = new ExtendedLoggerWrapper((AbstractLogger) logger, logger.getName(), logger.getMessageFactory());
|
||||
}
|
||||
|
||||
public static LoggerTerminal getLogger(String logger) {
|
||||
return new LoggerTerminal(LogManager.getLogger(logger));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHeadless() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String readText(String prompt) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] readSecret(String prompt) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public char[] readSecret(String prompt, int maxLength) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter getWriter() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter getErrorWriter() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void print(Verbosity verbosity, String msg, boolean isError) {
|
||||
Level level;
|
||||
switch (verbosity) {
|
||||
case SILENT:
|
||||
level = isError ? Level.ERROR : Level.WARN;
|
||||
break;
|
||||
|
||||
case VERBOSE:
|
||||
level = Level.DEBUG;
|
||||
break;
|
||||
|
||||
case NORMAL:
|
||||
default:
|
||||
level = isError ? Level.WARN : Level.INFO;
|
||||
break;
|
||||
}
|
||||
this.logger.logIfEnabled(FQCN, level, null, msg.trim(), (Throwable) null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
|
@ -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
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.bootstrap.plugins;
|
||||
|
||||
import org.elasticsearch.cli.Terminal;
|
||||
import org.elasticsearch.env.Environment;
|
||||
import org.elasticsearch.plugins.PluginsSynchronizer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* This class is responsible for adding, updating or removing plugins so that the list of installed plugins
|
||||
* matches those in the {@code elasticsearch-plugins.yml} config file. It does this by loading a class
|
||||
* dynamically from the {@code plugin-cli} jar and executing it.
|
||||
*/
|
||||
public class PluginsManager {
|
||||
|
||||
public static final String SYNC_PLUGINS_ACTION = "org.elasticsearch.plugins.cli.SyncPluginsAction";
|
||||
|
||||
public static boolean configExists(Environment env) {
|
||||
return Files.exists(env.configFile().resolve("elasticsearch-plugins.yml"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes the currently-installed plugins.
|
||||
* @param env the environment to use
|
||||
* @throws Exception if anything goes wrong
|
||||
*/
|
||||
public static void syncPlugins(Environment env) throws Exception {
|
||||
ClassLoader classLoader = buildClassLoader(env);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final Class<PluginsSynchronizer> synchronizerClass = (Class<PluginsSynchronizer>) classLoader.loadClass(SYNC_PLUGINS_ACTION);
|
||||
|
||||
final PluginsSynchronizer provider = synchronizerClass.getConstructor(Terminal.class, Environment.class)
|
||||
.newInstance(LoggerTerminal.getLogger(SYNC_PLUGINS_ACTION), env);
|
||||
|
||||
provider.execute();
|
||||
}
|
||||
|
||||
private static ClassLoader buildClassLoader(Environment env) {
|
||||
final Path pluginLibDir = env.libFile().resolve("tools").resolve("plugin-cli");
|
||||
|
||||
try {
|
||||
final URL[] urls = Files.list(pluginLibDir)
|
||||
.filter(each -> each.getFileName().toString().endsWith(".jar"))
|
||||
.map(PluginsManager::pathToURL)
|
||||
.toArray(URL[]::new);
|
||||
|
||||
return URLClassLoader.newInstance(urls, PluginsManager.class.getClassLoader());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to list jars in [" + pluginLibDir + "]: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static URL pathToURL(Path path) {
|
||||
try {
|
||||
return path.toUri().toURL();
|
||||
} catch (MalformedURLException e) {
|
||||
// Shouldn't happen, but have to handle the exception
|
||||
throw new RuntimeException("Failed to convert path [" + path + "] to URL", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -322,10 +322,13 @@ public class PluginsService implements ReportingService<PluginsAndModules> {
|
|||
if (Files.exists(rootPath)) {
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(rootPath)) {
|
||||
for (Path plugin : stream) {
|
||||
if (FileSystemUtils.isDesktopServicesStore(plugin) || plugin.getFileName().toString().startsWith(".removing-")) {
|
||||
final String filename = plugin.getFileName().toString();
|
||||
if (FileSystemUtils.isDesktopServicesStore(plugin)
|
||||
|| filename.startsWith(".removing-")
|
||||
|| filename.equals(".elasticsearch-plugins.yml.cache")) {
|
||||
continue;
|
||||
}
|
||||
if (seen.add(plugin.getFileName().toString()) == false) {
|
||||
if (seen.add(filename) == false) {
|
||||
throw new IllegalStateException("duplicate plugin: " + plugin);
|
||||
}
|
||||
plugins.add(plugin);
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
package org.elasticsearch.plugins;
|
||||
|
||||
/**
|
||||
* This is a marker interface for classes that are capable of synchronizing the currently-installed ES plugins
|
||||
* with those that ought to be installed according to a configuration file.
|
||||
*/
|
||||
public interface PluginsSynchronizer {
|
||||
void execute() throws Exception;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue