mirror of
https://github.com/elastic/elasticsearch.git
synced 2025-06-28 17:34:17 -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
|
* build/distributions/local
|
||||||
* */
|
* */
|
||||||
import org.elasticsearch.gradle.Architecture
|
import org.elasticsearch.gradle.Architecture
|
||||||
|
import org.elasticsearch.gradle.VersionProperties
|
||||||
|
|
||||||
// gradle has an open issue of failing applying plugins in
|
// gradle has an open issue of failing applying plugins in
|
||||||
// precompiled script plugins (see https://github.com/gradle/gradle/issues/17004)
|
// precompiled script plugins (see https://github.com/gradle/gradle/issues/17004)
|
||||||
|
@ -29,6 +30,6 @@ tasks.register('localDistro', Sync) {
|
||||||
from(elasticsearch_distributions.local)
|
from(elasticsearch_distributions.local)
|
||||||
into("build/distribution/local")
|
into("build/distribution/local")
|
||||||
doLast {
|
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");
|
Map<String, TaskProvider<?>> versionTasks = versionTasks(project, "destructiveDistroUpgradeTest");
|
||||||
TaskProvider<Task> destructiveDistroTest = project.getTasks().register("destructiveDistroTest");
|
TaskProvider<Task> destructiveDistroTest = project.getTasks().register("destructiveDistroTest");
|
||||||
|
|
||||||
// Configuration examplePlugin = configureExamplePlugin(project);
|
Configuration examplePlugin = configureExamplePlugin(project);
|
||||||
|
|
||||||
List<TaskProvider<Test>> windowsTestTasks = new ArrayList<>();
|
List<TaskProvider<Test>> windowsTestTasks = new ArrayList<>();
|
||||||
Map<ElasticsearchDistributionType, List<TaskProvider<Test>>> linuxTestTasks = new HashMap<>();
|
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");
|
TaskProvider<?> depsTask = project.getTasks().register(taskname + "#deps");
|
||||||
// explicitly depend on the archive not on the implicit extracted distribution
|
// explicitly depend on the archive not on the implicit extracted distribution
|
||||||
depsTask.configure(t -> t.dependsOn(distribution.getArchiveDependencies()));
|
depsTask.configure(t -> t.dependsOn(distribution.getArchiveDependencies()));
|
||||||
|
depsTask.configure(t -> t.dependsOn(examplePlugin.getDependencies()));
|
||||||
depsTasks.put(taskname, depsTask);
|
depsTasks.put(taskname, depsTask);
|
||||||
TaskProvider<Test> destructiveTask = configureTestTask(project, taskname, distribution, t -> {
|
TaskProvider<Test> destructiveTask = configureTestTask(project, taskname, distribution, t -> {
|
||||||
t.onlyIf(t2 -> distribution.isDocker() == false || dockerSupport.get().getDockerAvailability().isAvailable);
|
t.onlyIf(t2 -> distribution.isDocker() == false || dockerSupport.get().getDockerAvailability().isAvailable);
|
||||||
addDistributionSysprop(t, DISTRIBUTION_SYSPROP, distribution::getFilepath);
|
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");
|
t.exclude("**/PackageUpgradeTests.class");
|
||||||
}, depsTask);
|
}, depsTask);
|
||||||
|
|
||||||
|
@ -314,7 +315,7 @@ public class DistroTestPlugin implements Plugin<Project> {
|
||||||
Configuration examplePlugin = project.getConfigurations().create(EXAMPLE_PLUGIN_CONFIGURATION);
|
Configuration examplePlugin = project.getConfigurations().create(EXAMPLE_PLUGIN_CONFIGURATION);
|
||||||
examplePlugin.getAttributes().attribute(ArtifactAttributes.ARTIFACT_FORMAT, ArtifactTypeDefinition.ZIP_TYPE);
|
examplePlugin.getAttributes().attribute(ArtifactAttributes.ARTIFACT_FORMAT, ArtifactTypeDefinition.ZIP_TYPE);
|
||||||
DependencyHandler deps = project.getDependencies();
|
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;
|
return examplePlugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
<message key="javadoc.missing" value="Types should explain their purpose" />
|
<message key="javadoc.missing" value="Types should explain their purpose" />
|
||||||
</module>
|
</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">
|
<module name="JavadocMethod">
|
||||||
<property name="severity" value="warning"/>
|
<property name="severity" value="warning"/>
|
||||||
<property name="accessModifiers" value="public"/>
|
<property name="accessModifiers" value="public"/>
|
||||||
|
|
|
@ -296,7 +296,7 @@ configure(subprojects.findAll { ['archives', 'packages'].contains(it.name) }) {
|
||||||
*****************************************************************************/
|
*****************************************************************************/
|
||||||
libFiles =
|
libFiles =
|
||||||
copySpec {
|
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)
|
from(configurations.libs)
|
||||||
into('tools/geoip-cli') {
|
into('tools/geoip-cli') {
|
||||||
from(configurations.libsGeoIpCli)
|
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.
|
// 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"
|
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 }
|
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 {} +
|
find config -type f -exec chmod 0664 {} +
|
||||||
|
|
||||||
<% if (docker_base == "cloud") { %>
|
<% 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/
|
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-s3-${version}.zip \\
|
||||||
file:/tmp/repository-gcs-${version}.zip \\
|
file:/tmp/repository-gcs-${version}.zip \\
|
||||||
file:/tmp/repository-azure-${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 */ %>
|
<% /* 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/
|
COPY filebeat-${version}.tar.gz metricbeat-${version}.tar.gz /tmp/
|
||||||
|
@ -125,8 +130,6 @@ RUN mkdir -p /opt/filebeat /opt/metricbeat && \\
|
||||||
|
|
||||||
# Add plugins infrastructure
|
# Add plugins infrastructure
|
||||||
RUN mkdir -p /opt/plugins/archive
|
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
|
RUN chmod -R 0555 /opt/plugins
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
|
|
@ -10,3 +10,4 @@ RUN chmod 0444 /opt/plugins/archive/*
|
||||||
FROM ${base_image}
|
FROM ${base_image}
|
||||||
|
|
||||||
COPY --from=builder /opt/plugins /opt/plugins
|
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 =========
|
// ========= config files =========
|
||||||
configurationFile '/etc/elasticsearch/elasticsearch.yml'
|
configurationFile '/etc/elasticsearch/elasticsearch.yml'
|
||||||
|
configurationFile '/etc/elasticsearch/elasticsearch-plugins.example.yml'
|
||||||
configurationFile '/etc/elasticsearch/jvm.options'
|
configurationFile '/etc/elasticsearch/jvm.options'
|
||||||
configurationFile '/etc/elasticsearch/log4j2.properties'
|
configurationFile '/etc/elasticsearch/log4j2.properties'
|
||||||
configurationFile '/etc/elasticsearch/role_mapping.yml'
|
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 {
|
dependencies {
|
||||||
compileOnly project(":server")
|
compileOnly project(":server")
|
||||||
compileOnly project(":libs:elasticsearch-cli")
|
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:bcpg-fips:1.0.4"
|
||||||
api "org.bouncycastle:bc-fips:1.0.2"
|
api "org.bouncycastle:bc-fips:1.0.2"
|
||||||
testImplementation project(":test:framework")
|
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.hash.MessageDigests;
|
||||||
import org.elasticsearch.common.io.Streams;
|
import org.elasticsearch.common.io.Streams;
|
||||||
import org.elasticsearch.common.util.set.Sets;
|
import org.elasticsearch.common.util.set.Sets;
|
||||||
|
import org.elasticsearch.core.PathUtils;
|
||||||
import org.elasticsearch.core.SuppressForbidden;
|
import org.elasticsearch.core.SuppressForbidden;
|
||||||
import org.elasticsearch.core.Tuple;
|
import org.elasticsearch.core.Tuple;
|
||||||
import org.elasticsearch.core.internal.io.IOUtils;
|
import org.elasticsearch.core.internal.io.IOUtils;
|
||||||
|
@ -49,6 +50,7 @@ import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.UncheckedIOException;
|
import java.io.UncheckedIOException;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.Proxy;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
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
|
* elasticsearch config directory, using the name of the plugin. If any files to be installed
|
||||||
* already exist, they will be skipped.
|
* 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";
|
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. */
|
/** The official plugins that can be installed simply by name. */
|
||||||
static final Set<String> OFFICIAL_PLUGINS;
|
public static final Set<String> OFFICIAL_PLUGINS;
|
||||||
static {
|
static {
|
||||||
try (var stream = InstallPluginAction.class.getResourceAsStream("/plugins.txt")) {
|
try (var stream = InstallPluginAction.class.getResourceAsStream("/plugins.txt")) {
|
||||||
OFFICIAL_PLUGINS = Streams.readAllLines(stream).stream().map(String::trim).collect(Sets.toUnmodifiableSortedSet());
|
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 final Terminal terminal;
|
||||||
private Environment env;
|
private Environment env;
|
||||||
private boolean batch;
|
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.terminal = terminal;
|
||||||
this.env = env;
|
this.env = env;
|
||||||
this.batch = batch;
|
this.batch = batch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setProxy(Proxy proxy) {
|
||||||
|
this.proxy = proxy;
|
||||||
|
}
|
||||||
|
|
||||||
// pkg private for testing
|
// pkg private for testing
|
||||||
void execute(List<PluginDescriptor> plugins) throws Exception {
|
public void execute(List<PluginDescriptor> plugins) throws Exception {
|
||||||
if (plugins.isEmpty()) {
|
if (plugins.isEmpty()) {
|
||||||
throw new UserException(ExitCodes.USAGE, "at least one plugin id is required");
|
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<>();
|
final Map<String, List<Path>> deleteOnFailures = new LinkedHashMap<>();
|
||||||
for (final PluginDescriptor plugin : plugins) {
|
for (final PluginDescriptor plugin : plugins) {
|
||||||
final String pluginId = plugin.getId();
|
final String pluginId = plugin.getId();
|
||||||
terminal.println("-> Installing " + pluginId);
|
terminal.println(logPrefix + "Installing " + pluginId);
|
||||||
try {
|
try {
|
||||||
if ("x-pack".equals(pluginId)) {
|
if ("x-pack".equals(pluginId)) {
|
||||||
handleInstallXPack(buildFlavor());
|
handleInstallXPack(buildFlavor());
|
||||||
|
@ -216,15 +225,15 @@ class InstallPluginAction implements Closeable {
|
||||||
final Path pluginZip = download(plugin, env.tmpFile());
|
final Path pluginZip = download(plugin, env.tmpFile());
|
||||||
final Path extractedZip = unzip(pluginZip, env.pluginsFile());
|
final Path extractedZip = unzip(pluginZip, env.pluginsFile());
|
||||||
deleteOnFailure.add(extractedZip);
|
deleteOnFailure.add(extractedZip);
|
||||||
final PluginInfo pluginInfo = installPlugin(extractedZip, deleteOnFailure);
|
final PluginInfo pluginInfo = installPlugin(plugin, extractedZip, deleteOnFailure);
|
||||||
terminal.println("-> Installed " + pluginInfo.getName());
|
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
|
// 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.remove(pluginId);
|
||||||
deleteOnFailures.put(pluginInfo.getName(), deleteOnFailure);
|
deleteOnFailures.put(pluginInfo.getName(), deleteOnFailure);
|
||||||
} catch (final Exception installProblem) {
|
} 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()) {
|
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;
|
boolean success = false;
|
||||||
try {
|
try {
|
||||||
IOUtils.rm(deleteOnFailureEntry.getValue().toArray(new Path[0]));
|
IOUtils.rm(deleteOnFailureEntry.getValue().toArray(new Path[0]));
|
||||||
|
@ -235,17 +244,19 @@ class InstallPluginAction implements Closeable {
|
||||||
exceptionWhileRemovingFiles
|
exceptionWhileRemovingFiles
|
||||||
);
|
);
|
||||||
installProblem.addSuppressed(exception);
|
installProblem.addSuppressed(exception);
|
||||||
terminal.println("-> Failed rolling back " + deleteOnFailureEntry.getKey());
|
terminal.println(logPrefix + "Failed rolling back " + deleteOnFailureEntry.getKey());
|
||||||
}
|
}
|
||||||
if (success) {
|
if (success) {
|
||||||
terminal.println("-> Rolled back " + deleteOnFailureEntry.getKey());
|
terminal.println(logPrefix + "Rolled back " + deleteOnFailureEntry.getKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw installProblem;
|
throw installProblem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (terminal.isHeadless() == false) {
|
||||||
terminal.println("-> Please restart Elasticsearch to activate any plugins installed");
|
terminal.println("-> Please restart Elasticsearch to activate any plugins installed");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Build.Flavor buildFlavor() {
|
Build.Flavor buildFlavor() {
|
||||||
return Build.CURRENT.flavor();
|
return Build.CURRENT.flavor();
|
||||||
|
@ -271,24 +282,37 @@ class InstallPluginAction implements Closeable {
|
||||||
private Path download(PluginDescriptor plugin, Path tmpDir) throws Exception {
|
private Path download(PluginDescriptor plugin, Path tmpDir) throws Exception {
|
||||||
final String pluginId = plugin.getId();
|
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);
|
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);
|
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
|
// now try as maven coordinates, a valid URL would only have a colon and slash
|
||||||
String[] coordinates = pluginUrl.split(":");
|
String[] coordinates = pluginLocation.split(":");
|
||||||
if (coordinates.length == 3 && pluginUrl.contains("/") == false && pluginUrl.startsWith("file:") == false) {
|
if (coordinates.length == 3 && pluginLocation.contains("/") == false && pluginLocation.startsWith("file:") == false) {
|
||||||
String mavenUrl = getMavenUrl(coordinates, Platforms.PLATFORM_NAME);
|
String mavenUrl = getMavenUrl(coordinates);
|
||||||
terminal.println("-> Downloading " + pluginId + " from maven central");
|
terminal.println(logPrefix + "Downloading " + pluginId + " from maven central");
|
||||||
return downloadAndValidate(mavenUrl, tmpDir, false);
|
return downloadAndValidate(mavenUrl, tmpDir, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// fall back to plain old URL
|
// 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
|
// definitely not a valid url, so assume it is a plugin name
|
||||||
List<String> pluginSuggestions = checkMisspelledPlugin(pluginId);
|
List<String> pluginSuggestions = checkMisspelledPlugin(pluginId);
|
||||||
String msg = "Unknown plugin " + pluginId;
|
String msg = "Unknown plugin " + pluginId;
|
||||||
|
@ -297,8 +321,20 @@ class InstallPluginAction implements Closeable {
|
||||||
}
|
}
|
||||||
throw new UserException(ExitCodes.USAGE, msg);
|
throw new UserException(ExitCodes.USAGE, msg);
|
||||||
}
|
}
|
||||||
terminal.println("-> Downloading " + URLDecoder.decode(pluginUrl, StandardCharsets.UTF_8));
|
terminal.println(logPrefix + "Downloading " + URLDecoder.decode(pluginLocation, StandardCharsets.UTF_8));
|
||||||
return downloadZip(pluginUrl, tmpDir);
|
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
|
// pkg private so tests can override
|
||||||
|
@ -364,12 +400,12 @@ class InstallPluginAction implements Closeable {
|
||||||
/**
|
/**
|
||||||
* Returns the url for an elasticsearch plugin in maven.
|
* 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 groupId = coordinates[0].replace(".", "/");
|
||||||
final String artifactId = coordinates[1];
|
final String artifactId = coordinates[1];
|
||||||
final String version = coordinates[2];
|
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 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)) {
|
if (urlExists(platformUrl)) {
|
||||||
return platformUrl;
|
return platformUrl;
|
||||||
}
|
}
|
||||||
|
@ -407,7 +443,7 @@ class InstallPluginAction implements Closeable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CollectionUtil.timSort(scoredKeys, (a, b) -> b.v1().compareTo(a.v1()));
|
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. */
|
/** 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);
|
terminal.println(VERBOSE, "Retrieving zip from " + urlString);
|
||||||
URL url = new URL(urlString);
|
URL url = new URL(urlString);
|
||||||
Path zip = Files.createTempFile(tmpDir, null, ".zip");
|
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");
|
urlConnection.addRequestProperty("User-Agent", "elasticsearch-plugin-installer");
|
||||||
try (
|
try (
|
||||||
InputStream in = batch
|
InputStream in = batch || terminal.isHeadless()
|
||||||
? urlConnection.getInputStream()
|
? urlConnection.getInputStream()
|
||||||
: new TerminalProgressInputStream(urlConnection.getInputStream(), urlConnection.getContentLength(), terminal)
|
: 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
|
* 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 final Terminal terminal;
|
||||||
private int width = 50;
|
|
||||||
private final boolean enabled;
|
private final boolean enabled;
|
||||||
|
|
||||||
TerminalProgressInputStream(InputStream is, int expectedTotalSize, Terminal terminal) {
|
TerminalProgressInputStream(InputStream is, int expectedTotalSize, Terminal terminal) {
|
||||||
|
@ -458,13 +494,13 @@ class InstallPluginAction implements Closeable {
|
||||||
@Override
|
@Override
|
||||||
public void onProgress(int percent) {
|
public void onProgress(int percent) {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
int currentPosition = percent * width / 100;
|
int currentPosition = percent * WIDTH / 100;
|
||||||
StringBuilder sb = new StringBuilder("\r[");
|
StringBuilder sb = new StringBuilder("\r[");
|
||||||
sb.append(String.join("=", Collections.nCopies(currentPosition, "")));
|
sb.append(String.join("=", Collections.nCopies(currentPosition, "")));
|
||||||
if (currentPosition > 0 && percent < 100) {
|
if (currentPosition > 0 && percent < 100) {
|
||||||
sb.append(">");
|
sb.append(">");
|
||||||
}
|
}
|
||||||
sb.append(String.join(" ", Collections.nCopies(width - currentPosition, "")));
|
sb.append(String.join(" ", Collections.nCopies(WIDTH - currentPosition, "")));
|
||||||
sb.append("] %s ");
|
sb.append("] %s ");
|
||||||
if (percent == 100) {
|
if (percent == 100) {
|
||||||
sb.append("\n");
|
sb.append("\n");
|
||||||
|
@ -476,7 +512,7 @@ class InstallPluginAction implements Closeable {
|
||||||
|
|
||||||
@SuppressForbidden(reason = "URL#openStream")
|
@SuppressForbidden(reason = "URL#openStream")
|
||||||
private InputStream urlOpenStream(final URL url) throws IOException {
|
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
|
* 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.
|
* file contains a single line.
|
||||||
*/
|
*/
|
||||||
|
final BufferedReader checksumReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||||
if (digestAlgo.equals("SHA-1")) {
|
if (digestAlgo.equals("SHA-1")) {
|
||||||
final BufferedReader checksumReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
|
||||||
expectedChecksum = checksumReader.readLine();
|
expectedChecksum = checksumReader.readLine();
|
||||||
if (checksumReader.readLine() != null) {
|
|
||||||
throw new UserException(ExitCodes.IO_ERROR, "Invalid checksum file at " + checksumUrl);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
final BufferedReader checksumReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
|
||||||
final String checksumLine = checksumReader.readLine();
|
final String checksumLine = checksumReader.readLine();
|
||||||
final String[] fields = checksumLine.split(" {2}");
|
final String[] fields = checksumLine.split(" {2}");
|
||||||
if (officialPlugin && fields.length != 2 || officialPlugin == false && fields.length > 2) {
|
if (officialPlugin && fields.length != 2 || officialPlugin == false && fields.length > 2) {
|
||||||
|
@ -557,11 +589,11 @@ class InstallPluginAction implements Closeable {
|
||||||
throw new UserException(ExitCodes.IO_ERROR, message);
|
throw new UserException(ExitCodes.IO_ERROR, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (checksumReader.readLine() != null) {
|
if (checksumReader.readLine() != null) {
|
||||||
throw new UserException(ExitCodes.IO_ERROR, "Invalid checksum file at " + checksumUrl);
|
throw new UserException(ExitCodes.IO_ERROR, "Invalid checksum file at " + checksumUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// read the bytes of the plugin zip in chunks to avoid out of memory errors
|
// read the bytes of the plugin zip in chunks to avoid out of memory errors
|
||||||
try (InputStream zis = Files.newInputStream(zip)) {
|
try (InputStream zis = Files.newInputStream(zip)) {
|
||||||
|
@ -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.
|
* ".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 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 IOException if an I/O exception occurs reading from various input streams
|
||||||
* @throws PGPException if the PGP implementation throws an internal exception during verification
|
* @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.
|
* Creates a URL and opens a connection.
|
||||||
* <p>
|
* <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
|
// pkg private for tests
|
||||||
URL openUrl(String urlString) throws IOException {
|
URL openUrl(String urlString) throws IOException {
|
||||||
URL checksumUrl = new URL(urlString);
|
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) {
|
if (connection.getResponseCode() == 404) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -832,7 +866,7 @@ class InstallPluginAction implements Closeable {
|
||||||
* Installs the plugin from {@code tmpRoot} into the plugins dir.
|
* 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.
|
* 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);
|
final PluginInfo info = loadPluginInfo(tmpRoot);
|
||||||
checkCanInstallationProceed(terminal, Build.CURRENT.flavor(), info);
|
checkCanInstallationProceed(terminal, Build.CURRENT.flavor(), info);
|
||||||
PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(tmpRoot, env.tmpFile());
|
PluginPolicyInfo pluginPolicy = PolicyUtil.getPluginPolicyInfo(tmpRoot, env.tmpFile());
|
||||||
|
@ -841,6 +875,16 @@ class InstallPluginAction implements Closeable {
|
||||||
PluginSecurity.confirmPolicyExceptions(terminal, permissions, batch);
|
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());
|
final Path destination = env.pluginsFile().resolve(info.getName());
|
||||||
deleteOnFailure.add(destination);
|
deleteOnFailure.add(destination);
|
||||||
|
|
||||||
|
@ -876,10 +920,10 @@ class InstallPluginAction implements Closeable {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves the plugin directory into its final destination.
|
* Moves the plugin directory into its final destination.
|
||||||
**/
|
*/
|
||||||
private void movePlugin(Path tmpRoot, Path destination) throws IOException {
|
private void movePlugin(Path tmpRoot, Path destination) throws IOException {
|
||||||
Files.move(tmpRoot, destination, StandardCopyOption.ATOMIC_MOVE);
|
Files.move(tmpRoot, destination, StandardCopyOption.ATOMIC_MOVE);
|
||||||
Files.walkFileTree(destination, new SimpleFileVisitor<Path>() {
|
Files.walkFileTree(destination, new SimpleFileVisitor<>() {
|
||||||
@Override
|
@Override
|
||||||
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
|
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
|
||||||
final String parentDirName = file.getParent().getFileName().toString();
|
final String parentDirName = file.getParent().getFileName().toString();
|
||||||
|
@ -991,10 +1035,10 @@ class InstallPluginAction implements Closeable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
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) {
|
if (info.isLicensed() == false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,13 +75,17 @@ class InstallPluginCommand extends EnvironmentAwareCommand {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
protected void execute(Terminal terminal, OptionSet options, Environment env) throws Exception {
|
||||||
|
SyncPluginsAction.ensureNoConfigFile(env);
|
||||||
|
|
||||||
List<PluginDescriptor> plugins = arguments.values(options)
|
List<PluginDescriptor> plugins = arguments.values(options)
|
||||||
.stream()
|
.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());
|
.collect(Collectors.toList());
|
||||||
final boolean isBatch = options.has(batchOption);
|
final boolean isBatch = options.has(batchOption);
|
||||||
|
|
||||||
InstallPluginAction action = new InstallPluginAction(terminal, env, isBatch);
|
try (InstallPluginAction action = new InstallPluginAction(terminal, env, isBatch)) {
|
||||||
action.execute(plugins);
|
action.execute(plugins);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,8 @@ import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
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.
|
* 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());
|
terminal.println(Terminal.Verbosity.VERBOSE, "Plugins directory: " + env.pluginsFile());
|
||||||
final List<Path> plugins = new ArrayList<>();
|
final List<Path> plugins = new ArrayList<>();
|
||||||
try (DirectoryStream<Path> paths = Files.newDirectoryStream(env.pluginsFile())) {
|
try (DirectoryStream<Path> paths = Files.newDirectoryStream(env.pluginsFile())) {
|
||||||
for (Path plugin : paths) {
|
for (Path path : paths) {
|
||||||
plugins.add(plugin);
|
if (path.getFileName().toString().equals(ELASTICSEARCH_PLUGINS_YML_CACHE) == false) {
|
||||||
|
plugins.add(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Collections.sort(plugins);
|
Collections.sort(plugins);
|
||||||
|
|
|
@ -10,25 +10,29 @@ package org.elasticsearch.plugins.cli;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Models a single plugin that can be installed.
|
||||||
|
*/
|
||||||
public class PluginDescriptor {
|
public class PluginDescriptor {
|
||||||
private String id;
|
private String id;
|
||||||
private String url;
|
private String location;
|
||||||
private String proxy;
|
|
||||||
|
|
||||||
public PluginDescriptor() {}
|
public PluginDescriptor() {}
|
||||||
|
|
||||||
public PluginDescriptor(String id, String url, String proxy) {
|
/**
|
||||||
this.id = id;
|
* Creates a new descriptor instance.
|
||||||
this.url = url;
|
*
|
||||||
this.proxy = proxy;
|
* @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 url) {
|
*/
|
||||||
this(id, url, null);
|
public PluginDescriptor(String id, String location) {
|
||||||
|
this.id = Objects.requireNonNull(id, "id cannot be null");
|
||||||
|
this.location = location;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PluginDescriptor(String id) {
|
public PluginDescriptor(String id) {
|
||||||
this(id, null, null);
|
this(id, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
|
@ -39,20 +43,12 @@ public class PluginDescriptor {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUrl() {
|
public String getLocation() {
|
||||||
return url;
|
return location;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUrl(String url) {
|
public void setLocation(String location) {
|
||||||
this.url = url;
|
this.location = location;
|
||||||
}
|
|
||||||
|
|
||||||
public String getProxy() {
|
|
||||||
return proxy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProxy(String proxy) {
|
|
||||||
this.proxy = proxy;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -60,11 +56,16 @@ public class PluginDescriptor {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
PluginDescriptor that = (PluginDescriptor) o;
|
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
|
@Override
|
||||||
public int hashCode() {
|
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.Set;
|
||||||
import java.util.stream.Collectors;
|
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 {
|
public class PluginSecurity {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,10 +41,20 @@ public class PluginSecurity {
|
||||||
if (requested.isEmpty()) {
|
if (requested.isEmpty()) {
|
||||||
terminal.println(Verbosity.VERBOSE, "plugin has a policy file with no additional permissions");
|
terminal.println(Verbosity.VERBOSE, "plugin has a policy file with no additional permissions");
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// sort permissions in a reasonable order
|
// sort permissions in a reasonable order
|
||||||
Collections.sort(requested);
|
Collections.sort(requested);
|
||||||
|
|
||||||
|
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, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
|
||||||
terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @");
|
terminal.errorPrintln(Verbosity.NORMAL, "@ WARNING: plugin requires additional permissions @");
|
||||||
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
|
terminal.errorPrintln(Verbosity.NORMAL, "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@");
|
||||||
|
@ -48,21 +62,26 @@ public class PluginSecurity {
|
||||||
for (String permission : requested) {
|
for (String permission : requested) {
|
||||||
terminal.errorPrintln(Verbosity.NORMAL, "* " + permission);
|
terminal.errorPrintln(Verbosity.NORMAL, "* " + permission);
|
||||||
}
|
}
|
||||||
terminal.errorPrintln(Verbosity.NORMAL, "See http://docs.oracle.com/javase/8/docs/technotes/guides/security/permissions.html");
|
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.");
|
terminal.errorPrintln(Verbosity.NORMAL, "for descriptions of what these permissions allow and the associated risks.");
|
||||||
prompt(terminal, batch);
|
|
||||||
|
if (batch == false) {
|
||||||
|
prompt(terminal);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void prompt(final Terminal terminal, final boolean batch) throws UserException {
|
private static void prompt(final Terminal terminal) throws UserException {
|
||||||
if (batch == false) {
|
|
||||||
terminal.println(Verbosity.NORMAL, "");
|
terminal.println(Verbosity.NORMAL, "");
|
||||||
String text = terminal.readText("Continue with installation? [y/N]");
|
String text = terminal.readText("Continue with installation? [y/N]");
|
||||||
if (text.equalsIgnoreCase("y") == false) {
|
if (text.equalsIgnoreCase("y") == false) {
|
||||||
throw new UserException(ExitCodes.DATA_ERROR, "installation aborted by user");
|
throw new UserException(ExitCodes.DATA_ERROR, "installation aborted by user");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** Format permission type, name, and actions into a string */
|
/** Format permission type, name, and actions into a string */
|
||||||
static String formatPermission(Permission permission) {
|
static String formatPermission(Permission permission) {
|
||||||
|
@ -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.
|
* 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));
|
Set<Permission> allPermissions = new HashSet<>(PolicyUtil.getPolicyPermissions(null, pluginPolicyInfo.policy, tmpDir));
|
||||||
for (URL jar : pluginPolicyInfo.jars) {
|
for (URL jar : pluginPolicyInfo.jars) {
|
||||||
Set<Permission> jarPermissions = PolicyUtil.getPolicyPermissions(jar, pluginPolicyInfo.policy, tmpDir);
|
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
|
* 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 final int expectedTotalSize;
|
||||||
private int currentPercent;
|
private int currentPercent;
|
||||||
private int count = 0;
|
private int count = 0;
|
||||||
|
|
||||||
ProgressInputStream(InputStream is, int expectedTotalSize) {
|
public ProgressInputStream(InputStream is, int expectedTotalSize) {
|
||||||
super(is);
|
super(is);
|
||||||
this.expectedTotalSize = expectedTotalSize;
|
this.expectedTotalSize = expectedTotalSize;
|
||||||
this.currentPercent = 0;
|
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.
|
* An action for the plugin CLI to remove plugins from Elasticsearch.
|
||||||
*/
|
*/
|
||||||
class RemovePluginAction {
|
public class RemovePluginAction {
|
||||||
|
|
||||||
// exit codes for remove
|
// exit codes for remove
|
||||||
/** A plugin cannot be removed because it is extended by another plugin. */
|
/** A plugin cannot be removed because it is extended by another plugin. */
|
||||||
|
@ -42,7 +42,7 @@ class RemovePluginAction {
|
||||||
|
|
||||||
private final Terminal terminal;
|
private final Terminal terminal;
|
||||||
private final Environment env;
|
private final Environment env;
|
||||||
private final boolean purge;
|
private boolean purge;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new action.
|
* Creates a new action.
|
||||||
|
@ -51,12 +51,16 @@ class RemovePluginAction {
|
||||||
* @param env the environment for the local node
|
* @param env the environment for the local node
|
||||||
* @param purge if true, plugin configuration files will be removed but otherwise preserved
|
* @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.terminal = terminal;
|
||||||
this.env = env;
|
this.env = env;
|
||||||
this.purge = purge;
|
this.purge = purge;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPurge(boolean purge) {
|
||||||
|
this.purge = purge;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the plugin specified by {@code pluginName}.
|
* Remove the plugin specified by {@code pluginName}.
|
||||||
*
|
*
|
||||||
|
@ -66,7 +70,7 @@ class RemovePluginAction {
|
||||||
* @throws UserException if plugin directory does not exist
|
* @throws UserException if plugin directory does not exist
|
||||||
* @throws UserException if the plugin bin directory is not a directory
|
* @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()) {
|
if (plugins == null || plugins.isEmpty()) {
|
||||||
throw new UserException(ExitCodes.USAGE, "At least one plugin ID is required");
|
throw new UserException(ExitCodes.USAGE, "At least one plugin ID is required");
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,8 @@ class RemovePluginCommand extends EnvironmentAwareCommand {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void execute(final Terminal terminal, final OptionSet options, final Environment env) throws Exception {
|
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 List<PluginDescriptor> plugins = arguments.values(options).stream().map(PluginDescriptor::new).collect(Collectors.toList());
|
||||||
|
|
||||||
final RemovePluginAction action = new RemovePluginAction(terminal, env, options.has(purgeOption));
|
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");
|
Path zip = createTempDir().resolve(structure.getFileName() + ".zip");
|
||||||
try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zip))) {
|
try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zip))) {
|
||||||
forEachFileRecursively(structure, (file, attrs) -> {
|
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));
|
stream.putNextEntry(new ZipEntry(target));
|
||||||
Files.copy(file, stream);
|
Files.copy(file, stream);
|
||||||
});
|
});
|
||||||
|
@ -415,20 +415,20 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
public void testDuplicateInstall() throws Exception {
|
public void testDuplicateInstall() throws Exception {
|
||||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||||
final UserException e = expectThrows(UserException.class, () -> installPlugins(List.of(pluginZip, pluginZip), env.v1()));
|
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 {
|
public void testTransaction() throws Exception {
|
||||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||||
PluginDescriptor nonexistentPluginZip = new PluginDescriptor(
|
PluginDescriptor nonexistentPluginZip = new PluginDescriptor(
|
||||||
pluginZip.getId() + "-does-not-exist",
|
pluginZip.getId() + "-does-not-exist",
|
||||||
pluginZip.getUrl() + "-does-not-exist"
|
pluginZip.getLocation() + "-does-not-exist"
|
||||||
);
|
);
|
||||||
final FileNotFoundException e = expectThrows(
|
final FileNotFoundException e = expectThrows(
|
||||||
FileNotFoundException.class,
|
FileNotFoundException.class,
|
||||||
() -> installPlugins(List.of(pluginZip, nonexistentPluginZip), env.v1())
|
() -> 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");
|
final Path fakeInstallPath = env.v2().pluginsFile().resolve("fake");
|
||||||
// fake should have been removed when the file not found exception occurred
|
// fake should have been removed when the file not found exception occurred
|
||||||
assertFalse(Files.exists(fakeInstallPath));
|
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]",
|
"found file [%s] from a failed attempt to remove the plugin [failed]; execute [elasticsearch-plugin remove failed]",
|
||||||
removing
|
removing
|
||||||
);
|
);
|
||||||
assertThat(e, hasToString(containsString(expected)));
|
assertThat(e.getMessage(), containsString(expected));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testSpaceInUrl() throws Exception {
|
public void testSpaceInUrl() throws Exception {
|
||||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||||
Path pluginZipWithSpaces = createTempFile("foo bar", ".zip");
|
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);
|
Files.copy(in, pluginZipWithSpaces, StandardCopyOption.REPLACE_EXISTING);
|
||||||
}
|
}
|
||||||
PluginDescriptor modifiedPlugin = new PluginDescriptor("fake", pluginZipWithSpaces.toUri().toURL().toString());
|
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
|
// has two colons, so it appears similar to maven coordinates
|
||||||
PluginDescriptor plugin = new PluginDescriptor("fake", "://host:1234");
|
PluginDescriptor plugin = new PluginDescriptor("fake", "://host:1234");
|
||||||
MalformedURLException e = expectThrows(MalformedURLException.class, () -> installPlugin(plugin));
|
MalformedURLException e = expectThrows(MalformedURLException.class, () -> installPlugin(plugin));
|
||||||
assertTrue(e.getMessage(), e.getMessage().contains("no protocol"));
|
assertThat(e.getMessage(), containsString("no protocol"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testFileNotMaven() {
|
public void testFileNotMaven() {
|
||||||
|
@ -473,13 +473,13 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
// has two colons, so it appears similar to maven coordinates
|
// has two colons, so it appears similar to maven coordinates
|
||||||
() -> installPlugin("file:" + dir)
|
() -> installPlugin("file:" + dir)
|
||||||
);
|
);
|
||||||
assertFalse(e.getMessage(), e.getMessage().contains("maven.org"));
|
assertThat(e.getMessage(), not(containsString("maven.org")));
|
||||||
assertTrue(e.getMessage(), e.getMessage().contains(dir));
|
assertThat(e.getMessage(), containsString(dir));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testUnknownPlugin() {
|
public void testUnknownPlugin() {
|
||||||
UserException e = expectThrows(UserException.class, () -> installPlugin("foo"));
|
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 {
|
public void testPluginsDirReadOnly() throws Exception {
|
||||||
|
@ -488,7 +488,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
pluginsAttrs.setPermissions(new HashSet<>());
|
pluginsAttrs.setPermissions(new HashSet<>());
|
||||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||||
IOException e = expectThrows(IOException.class, () -> installPlugin(pluginZip));
|
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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
@ -496,7 +496,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
public void testBuiltinModule() throws Exception {
|
public void testBuiltinModule() throws Exception {
|
||||||
PluginDescriptor pluginZip = createPluginZip("lang-painless", pluginDir);
|
PluginDescriptor pluginZip = createPluginZip("lang-painless", pluginDir);
|
||||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,7 +506,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
// whose descriptor contains the name "x-pack".
|
// whose descriptor contains the name "x-pack".
|
||||||
pluginZip.setId("not-x-pack");
|
pluginZip.setId("not-x-pack");
|
||||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -517,7 +517,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
writeJar(pluginDirectory.resolve("other.jar"), "FakePlugin");
|
writeJar(pluginDirectory.resolve("other.jar"), "FakePlugin");
|
||||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDirectory); // adds plugin.jar with FakePlugin
|
PluginDescriptor pluginZip = createPluginZip("fake", pluginDirectory); // adds plugin.jar with FakePlugin
|
||||||
IllegalStateException e = expectThrows(IllegalStateException.class, () -> installPlugin(pluginZip, env.v1(), defaultAction));
|
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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -537,7 +537,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||||
installPlugin(pluginZip);
|
installPlugin(pluginZip);
|
||||||
UserException e = expectThrows(UserException.class, () -> 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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -555,7 +555,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
Files.createFile(binDir);
|
Files.createFile(binDir);
|
||||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -565,7 +565,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
Files.createFile(dirInBinDir.resolve("somescript"));
|
Files.createFile(dirInBinDir.resolve("somescript"));
|
||||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -575,7 +575,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
Files.createFile(binDir.resolve("somescript"));
|
Files.createFile(binDir.resolve("somescript"));
|
||||||
PluginDescriptor pluginZip = createPluginZip("elasticsearch", pluginDir);
|
PluginDescriptor pluginZip = createPluginZip("elasticsearch", pluginDir);
|
||||||
FileAlreadyExistsException e = expectThrows(FileAlreadyExistsException.class, () -> installPlugin(pluginZip));
|
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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -687,7 +687,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
Files.createFile(configDir);
|
Files.createFile(configDir);
|
||||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -697,7 +697,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
Files.createFile(dirInConfigDir.resolve("myconfig.yml"));
|
Files.createFile(dirInConfigDir.resolve("myconfig.yml"));
|
||||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir);
|
||||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -705,7 +705,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
Files.createFile(pluginDir.resolve("fake.yml"));
|
Files.createFile(pluginDir.resolve("fake.yml"));
|
||||||
String pluginZip = writeZip(pluginDir, null).toUri().toURL().toString();
|
String pluginZip = writeZip(pluginDir, null).toUri().toURL().toString();
|
||||||
NoSuchFileException e = expectThrows(NoSuchFileException.class, () -> installPlugin(pluginZip));
|
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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -724,18 +724,13 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
}
|
}
|
||||||
String pluginZip = zip.toUri().toURL().toString();
|
String pluginZip = zip.toUri().toURL().toString();
|
||||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
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());
|
assertInstallCleaned(env.v2());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testOfficialPluginsHelpSortedAndMissingObviouslyWrongPlugins() throws Exception {
|
public void testOfficialPluginsHelpSortedAndMissingObviouslyWrongPlugins() throws Exception {
|
||||||
MockTerminal terminal = new MockTerminal();
|
MockTerminal terminal = new MockTerminal();
|
||||||
new InstallPluginCommand() {
|
new MockInstallPluginCommand().main(new String[] { "--help" }, terminal);
|
||||||
@Override
|
|
||||||
protected boolean addShutdownHook() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}.main(new String[] { "--help" }, terminal);
|
|
||||||
try (BufferedReader reader = new BufferedReader(new StringReader(terminal.getOutput()))) {
|
try (BufferedReader reader = new BufferedReader(new StringReader(terminal.getOutput()))) {
|
||||||
String line = reader.readLine();
|
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"))));
|
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() {
|
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 {
|
private void installPlugin(boolean isBatch, String... additionalProperties) throws Exception {
|
||||||
// if batch is enabled, we also want to add a security policy
|
// if batch is enabled, we also want to add a security policy
|
||||||
if (isBatch) {
|
if (isBatch) {
|
||||||
|
@ -842,10 +849,36 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
skipJarHellAction.execute(List.of(pluginZip));
|
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")
|
@SuppressForbidden(reason = "Path.of() is OK in this context")
|
||||||
void assertInstallPluginFromUrl(
|
void assertInstallPluginFromUrl(
|
||||||
final String pluginId,
|
final String pluginId,
|
||||||
final String name,
|
final String pluginUrl,
|
||||||
final String url,
|
final String url,
|
||||||
final String stagingHash,
|
final String stagingHash,
|
||||||
final boolean isSnapshot,
|
final boolean isSnapshot,
|
||||||
|
@ -854,8 +887,8 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
final PGPSecretKey secretKey,
|
final PGPSecretKey secretKey,
|
||||||
final BiFunction<byte[], PGPSecretKey, String> signature
|
final BiFunction<byte[], PGPSecretKey, String> signature
|
||||||
) throws Exception {
|
) throws Exception {
|
||||||
PluginDescriptor pluginZip = createPlugin(name, pluginDir);
|
PluginDescriptor pluginZip = createPlugin(pluginId, pluginDir);
|
||||||
Path pluginZipPath = Path.of(URI.create(pluginZip.getUrl()));
|
Path pluginZipPath = Path.of(URI.create(pluginZip.getLocation()));
|
||||||
InstallPluginAction action = new InstallPluginAction(terminal, env.v2(), false) {
|
InstallPluginAction action = new InstallPluginAction(terminal, env.v2(), false) {
|
||||||
@Override
|
@Override
|
||||||
Path downloadZip(String urlString, Path tmpDir) throws IOException {
|
Path downloadZip(String urlString, Path tmpDir) throws IOException {
|
||||||
|
@ -868,7 +901,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
@Override
|
@Override
|
||||||
URL openUrl(String urlString) throws IOException {
|
URL openUrl(String urlString) throws IOException {
|
||||||
if ((url + shaExtension).equals(urlString)) {
|
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);
|
Path shaFile = temp.apply("shas").resolve("downloaded.zip" + shaExtension);
|
||||||
byte[] zipbytes = Files.readAllBytes(pluginZipPath);
|
byte[] zipbytes = Files.readAllBytes(pluginZipPath);
|
||||||
String checksum = shaCalculator.apply(zipbytes);
|
String checksum = shaCalculator.apply(zipbytes);
|
||||||
|
@ -886,7 +919,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void verifySignature(Path zip, String urlString) throws IOException, PGPException {
|
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);
|
super.verifySignature(zip, urlString);
|
||||||
} else {
|
} else {
|
||||||
throw new UnsupportedOperationException("verify signature should not be called for unofficial plugins");
|
throw new UnsupportedOperationException("verify signature should not be called for unofficial plugins");
|
||||||
|
@ -936,36 +969,15 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
// no jarhell check
|
// no jarhell check
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
installPlugin(new PluginDescriptor(name, pluginId), env.v1(), action);
|
installPlugin(new PluginDescriptor(pluginId, pluginUrl), env.v1(), action);
|
||||||
assertPlugin(name, pluginDir, env.v2());
|
assertPlugin(pluginId, 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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testOfficialPlugin() throws Exception {
|
public void testOfficialPlugin() throws Exception {
|
||||||
String url = "https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-"
|
String url = "https://artifacts.elastic.co/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-"
|
||||||
+ Build.CURRENT.getQualifiedVersion()
|
+ Build.CURRENT.getQualifiedVersion()
|
||||||
+ ".zip";
|
+ ".zip";
|
||||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, false);
|
assertInstallPluginFromUrl("analysis-icu", url, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testOfficialPluginSnapshot() throws Exception {
|
public void testOfficialPluginSnapshot() throws Exception {
|
||||||
|
@ -975,7 +987,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
Version.CURRENT,
|
Version.CURRENT,
|
||||||
Build.CURRENT.getQualifiedVersion()
|
Build.CURRENT.getQualifiedVersion()
|
||||||
);
|
);
|
||||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, "abc123", true);
|
assertInstallPluginFromUrl("analysis-icu", url, "abc123", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testInstallReleaseBuildOfPluginOnSnapshotBuild() {
|
public void testInstallReleaseBuildOfPluginOnSnapshotBuild() {
|
||||||
|
@ -985,15 +997,15 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
Version.CURRENT,
|
Version.CURRENT,
|
||||||
Build.CURRENT.getQualifiedVersion()
|
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(
|
final UserException e = expectThrows(
|
||||||
UserException.class,
|
UserException.class,
|
||||||
() -> assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, true)
|
() -> assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, true)
|
||||||
);
|
);
|
||||||
assertThat(e.exitCode, equalTo(ExitCodes.CONFIG));
|
assertThat(e.exitCode, equalTo(ExitCodes.CONFIG));
|
||||||
assertThat(
|
assertThat(
|
||||||
e,
|
e.getMessage(),
|
||||||
hasToString(containsString("attempted to install release build of official plugin on snapshot build of Elasticsearch"))
|
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-"
|
+ "-abc123/downloads/elasticsearch-plugins/analysis-icu/analysis-icu-"
|
||||||
+ Build.CURRENT.getQualifiedVersion()
|
+ Build.CURRENT.getQualifiedVersion()
|
||||||
+ ".zip";
|
+ ".zip";
|
||||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, "abc123", false);
|
assertInstallPluginFromUrl("analysis-icu", url, "abc123", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testOfficialPlatformPlugin() throws Exception {
|
public void testOfficialPlatformPlugin() throws Exception {
|
||||||
|
@ -1012,7 +1024,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
+ "-"
|
+ "-"
|
||||||
+ Build.CURRENT.getQualifiedVersion()
|
+ Build.CURRENT.getQualifiedVersion()
|
||||||
+ ".zip";
|
+ ".zip";
|
||||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, null, false);
|
assertInstallPluginFromUrl("analysis-icu", url, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testOfficialPlatformPluginSnapshot() throws Exception {
|
public void testOfficialPlatformPluginSnapshot() throws Exception {
|
||||||
|
@ -1023,7 +1035,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
Platforms.PLATFORM_NAME,
|
Platforms.PLATFORM_NAME,
|
||||||
Build.CURRENT.getQualifiedVersion()
|
Build.CURRENT.getQualifiedVersion()
|
||||||
);
|
);
|
||||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, "abc123", true);
|
assertInstallPluginFromUrl("analysis-icu", url, "abc123", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testOfficialPlatformPluginStaging() throws Exception {
|
public void testOfficialPlatformPluginStaging() throws Exception {
|
||||||
|
@ -1034,23 +1046,23 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
+ "-"
|
+ "-"
|
||||||
+ Build.CURRENT.getQualifiedVersion()
|
+ Build.CURRENT.getQualifiedVersion()
|
||||||
+ ".zip";
|
+ ".zip";
|
||||||
assertInstallPluginFromUrl("analysis-icu", "analysis-icu", url, "abc123", false);
|
assertInstallPluginFromUrl("analysis-icu", url, "abc123", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void testMavenPlugin() throws Exception {
|
public void testMavenPlugin() throws Exception {
|
||||||
String url = "https://repo1.maven.org/maven2/mygroup/myplugin/1.0.0/myplugin-1.0.0.zip";
|
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 {
|
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";
|
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 {
|
public void testMavenSha1Backcompat() throws Exception {
|
||||||
String url = "https://repo1.maven.org/maven2/mygroup/myplugin/1.0.0/myplugin-1.0.0.zip";
|
String url = "https://repo1.maven.org/maven2/mygroup/myplugin/1.0.0/myplugin-1.0.0.zip";
|
||||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
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"));
|
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";
|
String url = "https://repo1.maven.org/maven2/mygroup/myplugin/1.0.0/myplugin-1.0.0.zip";
|
||||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||||
assertInstallPluginFromUrl(
|
assertInstallPluginFromUrl(
|
||||||
"mygroup:myplugin:1.0.0",
|
|
||||||
"myplugin",
|
"myplugin",
|
||||||
|
"mygroup:myplugin:1.0.0",
|
||||||
url,
|
url,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
|
@ -1077,17 +1089,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||||
UserException e = expectThrows(
|
UserException e = expectThrows(
|
||||||
UserException.class,
|
UserException.class,
|
||||||
() -> assertInstallPluginFromUrl(
|
() -> assertInstallPluginFromUrl("analysis-icu", null, url, null, false, ".sha512", checksum(digest), null, (b, p) -> null)
|
||||||
"analysis-icu",
|
|
||||||
"analysis-icu",
|
|
||||||
url,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
".sha512",
|
|
||||||
checksum(digest),
|
|
||||||
null,
|
|
||||||
(b, p) -> null
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
||||||
assertThat(e.getMessage(), startsWith("Invalid checksum file"));
|
assertThat(e.getMessage(), startsWith("Invalid checksum file"));
|
||||||
|
@ -1100,20 +1102,10 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||||
UserException e = expectThrows(
|
UserException e = expectThrows(
|
||||||
UserException.class,
|
UserException.class,
|
||||||
() -> assertInstallPluginFromUrl(
|
() -> assertInstallPluginFromUrl("analysis-icu", null, url, null, false, ".sha1", checksum(digest), null, (b, p) -> null)
|
||||||
"analysis-icu",
|
|
||||||
"analysis-icu",
|
|
||||||
url,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
".sha1",
|
|
||||||
checksum(digest),
|
|
||||||
null,
|
|
||||||
(b, p) -> null
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
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() {
|
public void testMavenShaMissing() {
|
||||||
|
@ -1121,8 +1113,8 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
UserException e = expectThrows(
|
UserException e = expectThrows(
|
||||||
UserException.class,
|
UserException.class,
|
||||||
() -> assertInstallPluginFromUrl(
|
() -> assertInstallPluginFromUrl(
|
||||||
"mygroup:myplugin:1.0.0",
|
|
||||||
"myplugin",
|
"myplugin",
|
||||||
|
"mygroup:myplugin:1.0.0",
|
||||||
url,
|
url,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
|
@ -1133,7 +1125,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
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 {
|
public void testInvalidShaFileMissingFilename() throws Exception {
|
||||||
|
@ -1143,20 +1135,10 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||||
UserException e = expectThrows(
|
UserException e = expectThrows(
|
||||||
UserException.class,
|
UserException.class,
|
||||||
() -> assertInstallPluginFromUrl(
|
() -> assertInstallPluginFromUrl("analysis-icu", null, url, null, false, ".sha512", checksum(digest), null, (b, p) -> null)
|
||||||
"analysis-icu",
|
|
||||||
"analysis-icu",
|
|
||||||
url,
|
|
||||||
null,
|
|
||||||
false,
|
|
||||||
".sha512",
|
|
||||||
checksum(digest),
|
|
||||||
null,
|
|
||||||
(b, p) -> null
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
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 {
|
public void testInvalidShaFileMismatchFilename() throws Exception {
|
||||||
|
@ -1168,7 +1150,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
UserException.class,
|
UserException.class,
|
||||||
() -> assertInstallPluginFromUrl(
|
() -> assertInstallPluginFromUrl(
|
||||||
"analysis-icu",
|
"analysis-icu",
|
||||||
"analysis-icu",
|
null,
|
||||||
url,
|
url,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
|
@ -1191,7 +1173,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
UserException.class,
|
UserException.class,
|
||||||
() -> assertInstallPluginFromUrl(
|
() -> assertInstallPluginFromUrl(
|
||||||
"analysis-icu",
|
"analysis-icu",
|
||||||
"analysis-icu",
|
null,
|
||||||
url,
|
url,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
|
@ -1202,7 +1184,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
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() {
|
public void testSha512Mismatch() {
|
||||||
|
@ -1213,7 +1195,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
UserException.class,
|
UserException.class,
|
||||||
() -> assertInstallPluginFromUrl(
|
() -> assertInstallPluginFromUrl(
|
||||||
"analysis-icu",
|
"analysis-icu",
|
||||||
"analysis-icu",
|
null,
|
||||||
url,
|
url,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
|
@ -1224,7 +1206,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
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() {
|
public void testSha1Mismatch() {
|
||||||
|
@ -1232,8 +1214,8 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
UserException e = expectThrows(
|
UserException e = expectThrows(
|
||||||
UserException.class,
|
UserException.class,
|
||||||
() -> assertInstallPluginFromUrl(
|
() -> assertInstallPluginFromUrl(
|
||||||
"mygroup:myplugin:1.0.0",
|
|
||||||
"myplugin",
|
"myplugin",
|
||||||
|
"mygroup:myplugin:1.0.0",
|
||||||
url,
|
url,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
|
@ -1244,7 +1226,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
assertEquals(ExitCodes.IO_ERROR, e.exitCode);
|
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 {
|
public void testPublicKeyIdMismatchToExpectedPublicKeyId() throws Exception {
|
||||||
|
@ -1269,7 +1251,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
IllegalStateException.class,
|
IllegalStateException.class,
|
||||||
() -> assertInstallPluginFromUrl(
|
() -> assertInstallPluginFromUrl(
|
||||||
icu,
|
icu,
|
||||||
icu,
|
null,
|
||||||
url,
|
url,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
|
@ -1279,7 +1261,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
signature
|
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 {
|
public void testFailedSignatureVerification() throws Exception {
|
||||||
|
@ -1304,7 +1286,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
IllegalStateException.class,
|
IllegalStateException.class,
|
||||||
() -> assertInstallPluginFromUrl(
|
() -> assertInstallPluginFromUrl(
|
||||||
icu,
|
icu,
|
||||||
icu,
|
null,
|
||||||
url,
|
url,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
|
@ -1314,7 +1296,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
signature
|
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 {
|
public PGPSecretKey newSecretKey() throws NoSuchAlgorithmException, PGPException {
|
||||||
|
@ -1370,7 +1352,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
}
|
}
|
||||||
generator.generate().encode(pout);
|
generator.generate().encode(pout);
|
||||||
}
|
}
|
||||||
return new String(output.toByteArray(), "UTF-8");
|
return output.toString(StandardCharsets.UTF_8);
|
||||||
} catch (IOException | PGPException e) {
|
} catch (IOException | PGPException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
@ -1387,7 +1369,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
// default answer, does not install
|
// default answer, does not install
|
||||||
terminal.addTextInput("");
|
terminal.addTextInput("");
|
||||||
UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
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));
|
assertThat(terminal.getErrorOutput(), containsString("WARNING: " + warning));
|
||||||
try (Stream<Path> fileStream = Files.list(env.v2().pluginsFile())) {
|
try (Stream<Path> fileStream = Files.list(env.v2().pluginsFile())) {
|
||||||
|
@ -1401,7 +1383,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
}
|
}
|
||||||
terminal.addTextInput("n");
|
terminal.addTextInput("n");
|
||||||
e = expectThrows(UserException.class, () -> installPlugin(pluginZip));
|
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));
|
assertThat(terminal.getErrorOutput(), containsString("WARNING: " + warning));
|
||||||
try (Stream<Path> fileStream = Files.list(env.v2().pluginsFile())) {
|
try (Stream<Path> fileStream = Files.list(env.v2().pluginsFile())) {
|
||||||
assertThat(fileStream.collect(Collectors.toList()), empty());
|
assertThat(fileStream.collect(Collectors.toList()), empty());
|
||||||
|
@ -1431,7 +1413,7 @@ public class InstallPluginActionTests extends ESTestCase {
|
||||||
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir, "has.native.controller", "true");
|
PluginDescriptor pluginZip = createPluginZip("fake", pluginDir, "has.native.controller", "true");
|
||||||
|
|
||||||
final IllegalStateException e = expectThrows(IllegalStateException.class, () -> installPlugin(pluginZip));
|
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 {
|
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.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static java.util.Collections.emptyList;
|
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.not;
|
||||||
import static org.hamcrest.CoreMatchers.nullValue;
|
import static org.hamcrest.CoreMatchers.nullValue;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.hasToString;
|
|
||||||
|
|
||||||
@LuceneTestCase.SuppressFileSystems("*")
|
@LuceneTestCase.SuppressFileSystems("*")
|
||||||
public class RemovePluginActionTests extends ESTestCase {
|
public class RemovePluginActionTests extends ESTestCase {
|
||||||
|
@ -44,19 +42,6 @@ public class RemovePluginActionTests extends ESTestCase {
|
||||||
private Path home;
|
private Path home;
|
||||||
private Environment env;
|
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
|
@Override
|
||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
|
@ -121,7 +106,7 @@ public class RemovePluginActionTests extends ESTestCase {
|
||||||
|
|
||||||
public void testMissing() throws Exception {
|
public void testMissing() throws Exception {
|
||||||
UserException e = expectThrows(UserException.class, () -> removePlugin("dne", home, randomBoolean()));
|
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);
|
assertRemoveCleaned(env);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,7 +167,7 @@ public class RemovePluginActionTests extends ESTestCase {
|
||||||
createPlugin("fake");
|
createPlugin("fake");
|
||||||
Files.createFile(env.binFile().resolve("fake"));
|
Files.createFile(env.binFile().resolve("fake"));
|
||||||
UserException e = expectThrows(UserException.class, () -> removePlugin("fake", home, randomBoolean()));
|
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.pluginsFile().resolve("fake"))); // did not remove
|
||||||
assertTrue(Files.exists(env.binFile().resolve("fake")));
|
assertTrue(Files.exists(env.binFile().resolve("fake")));
|
||||||
assertRemoveCleaned(env);
|
assertRemoveCleaned(env);
|
||||||
|
@ -224,7 +209,7 @@ public class RemovePluginActionTests extends ESTestCase {
|
||||||
|
|
||||||
public void testPurgeNothingExists() throws Exception {
|
public void testPurgeNothingExists() throws Exception {
|
||||||
final UserException e = expectThrows(UserException.class, () -> removePlugin("fake", home, true));
|
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 {
|
public void testPurgeOnlyMarkerFileExists() throws Exception {
|
||||||
|
@ -276,7 +261,7 @@ public class RemovePluginActionTests extends ESTestCase {
|
||||||
|
|
||||||
e = expectThrows(UserException.class, () -> removePlugin(emptyList(), home, randomBoolean()));
|
e = expectThrows(UserException.class, () -> removePlugin(emptyList(), home, randomBoolean()));
|
||||||
assertEquals(ExitCodes.USAGE, e.exitCode);
|
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 {
|
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
|
* 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
|
* 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,
|
* 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
|
* only actually printing if the verbosity level of the terminal is above
|
||||||
* the verbosity of the message.
|
* 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. */
|
/** 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)) {
|
if (isPrintable(verbosity)) {
|
||||||
PrintWriter writer = isError ? getErrorWriter() : getWriter();
|
PrintWriter writer = isError ? getErrorWriter() : getWriter();
|
||||||
writer.print(msg);
|
writer.print(msg);
|
||||||
|
@ -206,6 +206,16 @@ public abstract class Terminal {
|
||||||
this.getErrorWriter().flush();
|
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 class ConsoleTerminal extends Terminal {
|
||||||
|
|
||||||
private static final Console CONSOLE = System.console();
|
private static final Console CONSOLE = System.console();
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
package org.elasticsearch.packaging.test;
|
package org.elasticsearch.packaging.test;
|
||||||
|
|
||||||
|
import com.carrotsearch.randomizedtesting.annotations.ThreadLeakFilters;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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;
|
||||||
import org.elasticsearch.packaging.util.Shell.Result;
|
import org.elasticsearch.packaging.util.Shell.Result;
|
||||||
import org.elasticsearch.packaging.util.docker.DockerRun;
|
import org.elasticsearch.packaging.util.docker.DockerRun;
|
||||||
|
import org.elasticsearch.packaging.util.docker.MockServer;
|
||||||
import org.junit.After;
|
import org.junit.After;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.BeforeClass;
|
import org.junit.BeforeClass;
|
||||||
|
@ -32,6 +34,7 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.StringJoiner;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
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.p755;
|
||||||
import static org.elasticsearch.packaging.util.FileMatcher.p775;
|
import static org.elasticsearch.packaging.util.FileMatcher.p775;
|
||||||
import static org.elasticsearch.packaging.util.FileUtils.append;
|
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.FileUtils.rm;
|
||||||
import static org.elasticsearch.packaging.util.docker.Docker.chownWithPrivilegeEscalation;
|
import static org.elasticsearch.packaging.util.docker.Docker.chownWithPrivilegeEscalation;
|
||||||
import static org.elasticsearch.packaging.util.docker.Docker.copyFromContainer;
|
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.allOf;
|
||||||
import static org.hamcrest.Matchers.arrayContaining;
|
import static org.hamcrest.Matchers.arrayContaining;
|
||||||
import static org.hamcrest.Matchers.contains;
|
import static org.hamcrest.Matchers.contains;
|
||||||
|
import static org.hamcrest.Matchers.containsInAnyOrder;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.emptyString;
|
import static org.hamcrest.Matchers.emptyString;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.hasEntry;
|
||||||
import static org.hamcrest.Matchers.hasItems;
|
import static org.hamcrest.Matchers.hasItems;
|
||||||
import static org.hamcrest.Matchers.hasKey;
|
import static org.hamcrest.Matchers.hasKey;
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
import static org.hamcrest.Matchers.matchesPattern;
|
import static org.hamcrest.Matchers.matchesPattern;
|
||||||
import static org.hamcrest.Matchers.not;
|
import static org.hamcrest.Matchers.not;
|
||||||
|
@ -92,10 +99,14 @@ import static org.junit.Assume.assumeTrue;
|
||||||
* <li>Images for Cloud</li>
|
* <li>Images for Cloud</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
*/
|
*/
|
||||||
|
@ThreadLeakFilters(defaultFilters = true, filters = { HttpClientThreadsFilter.class })
|
||||||
public class DockerTests extends PackagingTestCase {
|
public class DockerTests extends PackagingTestCase {
|
||||||
private Path tempDir;
|
private Path tempDir;
|
||||||
private static final String PASSWORD = "nothunter2";
|
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
|
@BeforeClass
|
||||||
public static void filterDistros() {
|
public static void filterDistros() {
|
||||||
assumeTrue("only Docker", distribution().isDocker());
|
assumeTrue("only Docker", distribution().isDocker());
|
||||||
|
@ -159,7 +170,7 @@ public class DockerTests extends PackagingTestCase {
|
||||||
/**
|
/**
|
||||||
* Check that Cloud images bundle a selection of plugins.
|
* Check that Cloud images bundle a selection of plugins.
|
||||||
*/
|
*/
|
||||||
public void test021PluginsListWithPlugins() {
|
public void test021PluginsListWithDefaultCloudPlugins() {
|
||||||
assumeTrue(
|
assumeTrue(
|
||||||
"Only applies to Cloud images",
|
"Only applies to Cloud images",
|
||||||
distribution.packaging == Packaging.DOCKER_CLOUD || distribution().packaging == Packaging.DOCKER_CLOUD_ESS
|
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() {
|
public void test022InstallPlugin() {
|
||||||
assumeTrue("Only applies to ESS images", distribution().packaging == Packaging.DOCKER_CLOUD_ESS);
|
runContainer(
|
||||||
|
distribution(),
|
||||||
|
builder().envVar("ELASTIC_PASSWORD", PASSWORD).volume(Path.of(EXAMPLE_PLUGIN_PATH), "/analysis-icu.zip")
|
||||||
|
);
|
||||||
|
|
||||||
final String plugin = "analysis-icu";
|
final String plugin = "analysis-icu";
|
||||||
|
assertThat("Expected " + plugin + " to not be installed", listPlugins(), not(hasItems(plugin)));
|
||||||
|
|
||||||
final Installation.Executables bin = installation.executables();
|
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
|
// Stuff the proxy settings with garbage, so any attempt to go out to the internet would fail
|
||||||
sh.getEnv()
|
sh.getEnv()
|
||||||
.put("ES_JAVA_OPTS", "-Dhttp.proxyHost=example.org -Dhttp.proxyPort=9999 -Dhttps.proxyHost=example.org -Dhttps.proxyPort=9999");
|
.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", listPlugins(), hasItems(plugin));
|
||||||
|
|
||||||
assertThat("Expected " + plugin + " to be installed", plugins, 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() {
|
public void test040JavaUsesTheOsProvidedKeystore() {
|
||||||
final String path = sh.run("realpath jdk/lib/security/cacerts").stdout;
|
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));
|
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;
|
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.Installation;
|
||||||
import org.elasticsearch.packaging.util.Platforms;
|
import org.elasticsearch.packaging.util.Platforms;
|
||||||
import org.elasticsearch.packaging.util.Shell;
|
import org.elasticsearch.packaging.util.Shell;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
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.assumeFalse;
|
||||||
import static org.junit.Assume.assumeTrue;
|
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 {
|
public class PluginCliTests extends PackagingTestCase {
|
||||||
|
|
||||||
private static final String EXAMPLE_PLUGIN_NAME = "custom-settings";
|
private static final String EXAMPLE_PLUGIN_NAME = "custom-settings";
|
||||||
|
@ -31,7 +34,7 @@ public class PluginCliTests extends PackagingTestCase {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// re-read before each test so the plugin path can be manipulated within tests
|
// 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
|
@Before
|
||||||
|
@ -46,7 +49,7 @@ public class PluginCliTests extends PackagingTestCase {
|
||||||
|
|
||||||
private Shell.Result assertWithPlugin(Installation.Executable pluginTool, Path pluginZip, String pluginName, PluginAction action)
|
private Shell.Result assertWithPlugin(Installation.Executable pluginTool, Path pluginZip, String pluginName, PluginAction action)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
Shell.Result installResult = pluginTool.run("install --batch \"" + pluginZip.toUri().toString() + "\"");
|
Shell.Result installResult = pluginTool.run("install --batch \"" + pluginZip.toUri() + "\"");
|
||||||
action.run(installResult);
|
action.run(installResult);
|
||||||
return pluginTool.run("remove " + pluginName);
|
return pluginTool.run("remove " + pluginName);
|
||||||
}
|
}
|
||||||
|
@ -70,15 +73,13 @@ public class PluginCliTests extends PackagingTestCase {
|
||||||
Platforms.onLinux(() -> sh.run("chown elasticsearch:elasticsearch " + linkedPlugins.toString()));
|
Platforms.onLinux(() -> sh.run("chown elasticsearch:elasticsearch " + linkedPlugins.toString()));
|
||||||
Files.createSymbolicLink(pluginsDir, linkedPlugins);
|
Files.createSymbolicLink(pluginsDir, linkedPlugins);
|
||||||
// Packaged installation don't get autoconfigured yet
|
// 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 -> {
|
assertWithExamplePlugin(installResult -> {
|
||||||
assertWhileRunning(() -> {
|
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));
|
assertThat(pluginsResponse, equalTo(EXAMPLE_PLUGIN_NAME));
|
||||||
|
|
||||||
String settingsPath = "_cluster/settings?include_defaults&filter_path=defaults.custom.simple";
|
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\"}}}"));
|
assertThat(settingsResponse, equalTo("{\"defaults\":{\"custom\":{\"simple\":\"foo\"}}}"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -119,4 +120,44 @@ public class PluginCliTests extends PackagingTestCase {
|
||||||
sh.setUmask("0077");
|
sh.setUmask("0077");
|
||||||
assertWithExamplePlugin(installResult -> {});
|
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 {
|
do {
|
||||||
try {
|
try {
|
||||||
// Give the container a chance to exit out
|
// 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) {
|
if (sh.run("docker ps --quiet --no-trunc").stdout.contains(containerId) == false) {
|
||||||
isElasticsearchRunning = false;
|
isElasticsearchRunning = false;
|
||||||
|
@ -462,8 +462,6 @@ public class Docker {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void verifyCloudContainerInstallation(Installation es) {
|
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 String pluginArchive = "/opt/plugins/archive";
|
||||||
final List<String> plugins = listContents(pluginArchive);
|
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.Distribution;
|
||||||
import org.elasticsearch.packaging.util.Platforms;
|
import org.elasticsearch.packaging.util.Platforms;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -53,6 +54,10 @@ public class DockerRun {
|
||||||
}
|
}
|
||||||
|
|
||||||
public DockerRun volume(Path from, String to) {
|
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)));
|
this.volumes.put(requireNonNull(from), Path.of(requireNonNull(to)));
|
||||||
return this;
|
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.logging.log4j.core.config.Configurator;
|
||||||
import org.apache.lucene.util.Constants;
|
import org.apache.lucene.util.Constants;
|
||||||
import org.apache.lucene.util.StringHelper;
|
import org.apache.lucene.util.StringHelper;
|
||||||
|
import org.elasticsearch.Build;
|
||||||
import org.elasticsearch.ElasticsearchException;
|
import org.elasticsearch.ElasticsearchException;
|
||||||
import org.elasticsearch.Version;
|
import org.elasticsearch.Version;
|
||||||
|
import org.elasticsearch.bootstrap.plugins.PluginsManager;
|
||||||
import org.elasticsearch.cli.UserException;
|
import org.elasticsearch.cli.UserException;
|
||||||
import org.elasticsearch.common.PidFile;
|
import org.elasticsearch.common.PidFile;
|
||||||
import org.elasticsearch.common.filesystem.FileSystemNatives;
|
import org.elasticsearch.common.filesystem.FileSystemNatives;
|
||||||
|
@ -339,6 +341,20 @@ final class Bootstrap {
|
||||||
// setDefaultUncaughtExceptionHandler
|
// setDefaultUncaughtExceptionHandler
|
||||||
Thread.setDefaultUncaughtExceptionHandler(new ElasticsearchUncaughtExceptionHandler());
|
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);
|
INSTANCE.setup(true, environment);
|
||||||
|
|
||||||
try {
|
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)) {
|
if (Files.exists(rootPath)) {
|
||||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(rootPath)) {
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(rootPath)) {
|
||||||
for (Path plugin : stream) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
if (seen.add(plugin.getFileName().toString()) == false) {
|
if (seen.add(filename) == false) {
|
||||||
throw new IllegalStateException("duplicate plugin: " + plugin);
|
throw new IllegalStateException("duplicate plugin: " + plugin);
|
||||||
}
|
}
|
||||||
plugins.add(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