diff --git a/buildSrc/src/main/resources/meta-plugin-descriptor.properties b/buildSrc/src/main/resources/meta-plugin-descriptor.properties new file mode 100644 index 00000000000..16dbe4c38a5 --- /dev/null +++ b/buildSrc/src/main/resources/meta-plugin-descriptor.properties @@ -0,0 +1,21 @@ +# Elasticsearch meta plugin descriptor file +# This file must exist as 'meta-plugin-descriptor.properties' in a folder named `elasticsearch`. +# +### example meta plugin for "meta-foo" +# +# meta-foo.zip <-- zip file for the meta plugin, with this structure: +#|____elasticsearch/ +#| |____ <-- The plugin files for bundled_plugin_1 (the content of the elastisearch directory) +#| |____ <-- The plugin files for bundled_plugin_2 +#| |____ meta-plugin-descriptor.properties <-- example contents below: +# +# description=My meta plugin +# name=meta-foo +# +### mandatory elements for all meta plugins: +# +# 'description': simple summary of the meta plugin +description=${description} +# +# 'name': the meta plugin name +name=${name} \ No newline at end of file diff --git a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/info/PluginsAndModules.java b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/info/PluginsAndModules.java index e562adf2602..67ee5756789 100644 --- a/core/src/main/java/org/elasticsearch/action/admin/cluster/node/info/PluginsAndModules.java +++ b/core/src/main/java/org/elasticsearch/action/admin/cluster/node/info/PluginsAndModules.java @@ -30,6 +30,7 @@ import org.elasticsearch.plugins.PluginInfo; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.List; /** @@ -60,23 +61,23 @@ public class PluginsAndModules implements Writeable, ToXContentFragment { */ public List getPluginInfos() { List plugins = new ArrayList<>(this.plugins); - Collections.sort(plugins, (p1, p2) -> p1.getName().compareTo(p2.getName())); + Collections.sort(plugins, Comparator.comparing(PluginInfo::getName)); return plugins; } - + /** * Returns an ordered list based on modules name */ public List getModuleInfos() { List modules = new ArrayList<>(this.modules); - Collections.sort(modules, (p1, p2) -> p1.getName().compareTo(p2.getName())); + Collections.sort(modules, Comparator.comparing(PluginInfo::getName)); return modules; } public void addPlugin(PluginInfo info) { plugins.add(info); } - + public void addModule(PluginInfo info) { modules.add(info); } diff --git a/core/src/main/java/org/elasticsearch/bootstrap/Security.java b/core/src/main/java/org/elasticsearch/bootstrap/Security.java index 89a1f794e89..e3de41c09c1 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/Security.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/Security.java @@ -163,16 +163,8 @@ final class Security { static Map getPluginPermissions(Environment environment) throws IOException, NoSuchAlgorithmException { Map map = new HashMap<>(); // collect up set of plugins and modules by listing directories. - Set pluginsAndModules = new LinkedHashSet<>(); // order is already lost, but some filesystems have it - if (Files.exists(environment.pluginsFile())) { - try (DirectoryStream stream = Files.newDirectoryStream(environment.pluginsFile())) { - for (Path plugin : stream) { - if (pluginsAndModules.add(plugin) == false) { - throw new IllegalStateException("duplicate plugin: " + plugin); - } - } - } - } + Set pluginsAndModules = new LinkedHashSet<>(PluginInfo.extractAllPlugins(environment.pluginsFile())); + if (Files.exists(environment.modulesFile())) { try (DirectoryStream stream = Files.newDirectoryStream(environment.modulesFile())) { for (Path module : stream) { diff --git a/core/src/main/java/org/elasticsearch/bootstrap/Spawner.java b/core/src/main/java/org/elasticsearch/bootstrap/Spawner.java index 0b9913f7f06..d6d66e18283 100644 --- a/core/src/main/java/org/elasticsearch/bootstrap/Spawner.java +++ b/core/src/main/java/org/elasticsearch/bootstrap/Spawner.java @@ -21,14 +21,12 @@ package org.elasticsearch.bootstrap; import org.apache.lucene.util.Constants; import org.apache.lucene.util.IOUtils; -import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.env.Environment; import org.elasticsearch.plugins.Platforms; import org.elasticsearch.plugins.PluginInfo; import java.io.Closeable; import java.io.IOException; -import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -72,27 +70,23 @@ final class Spawner implements Closeable { * For each plugin, attempt to spawn the controller daemon. Silently ignore any plugin that * don't include a controller for the correct platform. */ - try (DirectoryStream stream = Files.newDirectoryStream(pluginsFile)) { - for (final Path plugin : stream) { - if (FileSystemUtils.isDesktopServicesStore(plugin)) { - continue; - } - final PluginInfo info = PluginInfo.readFromProperties(plugin); - final Path spawnPath = Platforms.nativeControllerPath(plugin); - if (!Files.isRegularFile(spawnPath)) { - continue; - } - if (!info.hasNativeController()) { - final String message = String.format( - Locale.ROOT, - "plugin [%s] does not have permission to fork native controller", - plugin.getFileName()); - throw new IllegalArgumentException(message); - } - final Process process = - spawnNativePluginController(spawnPath, environment.tmpFile()); - processes.add(process); + List paths = PluginInfo.extractAllPlugins(pluginsFile); + for (Path plugin : paths) { + final PluginInfo info = PluginInfo.readFromProperties(plugin); + final Path spawnPath = Platforms.nativeControllerPath(plugin); + if (!Files.isRegularFile(spawnPath)) { + continue; } + if (!info.hasNativeController()) { + final String message = String.format( + Locale.ROOT, + "plugin [%s] does not have permission to fork native controller", + plugin.getFileName()); + throw new IllegalArgumentException(message); + } + final Process process = + spawnNativePluginController(spawnPath, environment.tmpFile()); + processes.add(process); } } diff --git a/core/src/main/java/org/elasticsearch/plugins/MetaPluginInfo.java b/core/src/main/java/org/elasticsearch/plugins/MetaPluginInfo.java new file mode 100644 index 00000000000..d8bb176273c --- /dev/null +++ b/core/src/main/java/org/elasticsearch/plugins/MetaPluginInfo.java @@ -0,0 +1,149 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.plugins; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Map; +import java.util.Properties; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * An in-memory representation of the meta plugin descriptor. + */ +public class MetaPluginInfo { + static final String ES_META_PLUGIN_PROPERTIES = "meta-plugin-descriptor.properties"; + + private final String name; + private final String description; + + /** + * Construct plugin info. + * + * @param name the name of the plugin + * @param description a description of the plugin + */ + private MetaPluginInfo(String name, String description) { + this.name = name; + this.description = description; + } + + /** + * @return Whether the provided {@code path} is a meta plugin. + */ + public static boolean isMetaPlugin(final Path path) { + return Files.exists(path.resolve(ES_META_PLUGIN_PROPERTIES)); + } + + /** + * @return Whether the provided {@code path} is a meta properties file. + */ + public static boolean isPropertiesFile(final Path path) { + return ES_META_PLUGIN_PROPERTIES.equals(path.getFileName().toString()); + } + + /** reads (and validates) meta plugin metadata descriptor file */ + + /** + * Reads and validates the meta plugin descriptor file. + * + * @param path the path to the root directory for the meta plugin + * @return the meta plugin info + * @throws IOException if an I/O exception occurred reading the meta plugin descriptor + */ + public static MetaPluginInfo readFromProperties(final Path path) throws IOException { + final Path descriptor = path.resolve(ES_META_PLUGIN_PROPERTIES); + + final Map propsMap; + { + final Properties props = new Properties(); + try (InputStream stream = Files.newInputStream(descriptor)) { + props.load(stream); + } + propsMap = props.stringPropertyNames().stream().collect(Collectors.toMap(Function.identity(), props::getProperty)); + } + + final String name = propsMap.remove("name"); + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException( + "property [name] is missing for meta plugin in [" + descriptor + "]"); + } + final String description = propsMap.remove("description"); + if (description == null) { + throw new IllegalArgumentException( + "property [description] is missing for meta plugin [" + name + "]"); + } + + if (propsMap.isEmpty() == false) { + throw new IllegalArgumentException("Unknown properties in meta plugin descriptor: " + propsMap.keySet()); + } + + return new MetaPluginInfo(name, description); + } + + /** + * The name of the meta plugin. + * + * @return the meta plugin name + */ + public String getName() { + return name; + } + + /** + * The description of the meta plugin. + * + * @return the meta plugin description + */ + public String getDescription() { + return description; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + MetaPluginInfo that = (MetaPluginInfo) o; + + if (!name.equals(that.name)) return false; + + return true; + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + final StringBuilder information = new StringBuilder() + .append("- Plugin information:\n") + .append("Name: ").append(name).append("\n") + .append("Description: ").append(description); + return information.toString(); + } + +} diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java b/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java index 01cc7ea6590..42c9df6d3dd 100644 --- a/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java +++ b/core/src/main/java/org/elasticsearch/plugins/PluginInfo.java @@ -22,7 +22,9 @@ package org.elasticsearch.plugins; import org.elasticsearch.Version; import org.elasticsearch.bootstrap.JarHell; import org.elasticsearch.common.Booleans; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; @@ -31,14 +33,19 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; import java.io.InputStream; +import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -125,7 +132,46 @@ public class PluginInfo implements Writeable, ToXContentObject { } } - /** reads (and validates) plugin metadata descriptor file */ + /** + * Extracts all {@link PluginInfo} from the provided {@code rootPath} expanding meta plugins if needed. + * @param rootPath the path where the plugins are installed + * @return A list of all plugin paths installed in the {@code rootPath} + * @throws IOException if an I/O exception occurred reading the plugin descriptors + */ + public static List extractAllPlugins(final Path rootPath) throws IOException { + final List plugins = new LinkedList<>(); // order is already lost, but some filesystems have it + final Set seen = new HashSet<>(); + if (Files.exists(rootPath)) { + try (DirectoryStream stream = Files.newDirectoryStream(rootPath)) { + for (Path plugin : stream) { + if (FileSystemUtils.isDesktopServicesStore(plugin) || + plugin.getFileName().toString().startsWith(".removing-")) { + continue; + } + if (seen.add(plugin.getFileName().toString()) == false) { + throw new IllegalStateException("duplicate plugin: " + plugin); + } + if (MetaPluginInfo.isMetaPlugin(plugin)) { + try (DirectoryStream subStream = Files.newDirectoryStream(plugin)) { + for (Path subPlugin : subStream) { + if (MetaPluginInfo.isPropertiesFile(subPlugin) || + FileSystemUtils.isDesktopServicesStore(subPlugin)) { + continue; + } + if (seen.add(subPlugin.getFileName().toString()) == false) { + throw new IllegalStateException("duplicate plugin: " + subPlugin); + } + plugins.add(subPlugin); + } + } + } else { + plugins.add(plugin); + } + } + } + } + return plugins; + } /** * Reads and validates the plugin descriptor file. @@ -341,16 +387,19 @@ public class PluginInfo implements Writeable, ToXContentObject { @Override public String toString() { - final StringBuilder information = new StringBuilder() - .append("- Plugin information:\n") - .append("Name: ").append(name).append("\n") - .append("Description: ").append(description).append("\n") - .append("Version: ").append(version).append("\n") - .append("Native Controller: ").append(hasNativeController).append("\n") - .append("Requires Keystore: ").append(requiresKeystore).append("\n") - .append("Extended Plugins: ").append(extendedPlugins).append("\n") - .append(" * Classname: ").append(classname); - return information.toString(); + return toString(""); } + public String toString(String prefix) { + final StringBuilder information = new StringBuilder() + .append(prefix).append("- Plugin information:\n") + .append(prefix).append("Name: ").append(name).append("\n") + .append(prefix).append("Description: ").append(description).append("\n") + .append(prefix).append("Version: ").append(version).append("\n") + .append(prefix).append("Native Controller: ").append(hasNativeController).append("\n") + .append(prefix).append("Requires Keystore: ").append(requiresKeystore).append("\n") + .append(prefix).append("Extended Plugins: ").append(extendedPlugins).append("\n") + .append(prefix).append(" * Classname: ").append(classname); + return information.toString(); + } } diff --git a/core/src/main/java/org/elasticsearch/plugins/PluginsService.java b/core/src/main/java/org/elasticsearch/plugins/PluginsService.java index 445edec1c19..d60d01273bb 100644 --- a/core/src/main/java/org/elasticsearch/plugins/PluginsService.java +++ b/core/src/main/java/org/elasticsearch/plugins/PluginsService.java @@ -34,7 +34,6 @@ import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.component.LifecycleComponent; import org.elasticsearch.common.inject.Module; -import org.elasticsearch.common.io.FileSystemUtils; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; @@ -322,29 +321,20 @@ public class PluginsService extends AbstractComponent { Logger logger = Loggers.getLogger(PluginsService.class); Set bundles = new LinkedHashSet<>(); - try (DirectoryStream stream = Files.newDirectoryStream(pluginsDirectory)) { - for (Path plugin : stream) { - if (FileSystemUtils.isDesktopServicesStore(plugin)) { - continue; - } - if (plugin.getFileName().toString().startsWith(".removing-")) { - continue; - } - logger.trace("--- adding plugin [{}]", plugin.toAbsolutePath()); - final PluginInfo info; - try { - info = PluginInfo.readFromProperties(plugin); - } catch (IOException e) { - throw new IllegalStateException("Could not load plugin descriptor for existing plugin [" - + plugin.getFileName() + "]. Was the plugin built before 2.0?", e); - } - - if (bundles.add(new Bundle(info, plugin)) == false) { - throw new IllegalStateException("duplicate plugin: " + info); - } + List infos = PluginInfo.extractAllPlugins(pluginsDirectory); + for (Path plugin : infos) { + logger.trace("--- adding plugin [{}]", plugin.toAbsolutePath()); + final PluginInfo info; + try { + info = PluginInfo.readFromProperties(plugin); + } catch (IOException e) { + throw new IllegalStateException("Could not load plugin descriptor for existing plugin [" + + plugin.getFileName() + "]. Was the plugin built before 2.0?", e); + } + if (bundles.add(new Bundle(info, plugin)) == false) { + throw new IllegalStateException("duplicate plugin: " + info); } } - return bundles; } diff --git a/core/src/test/java/org/elasticsearch/plugins/MetaPluginInfoTests.java b/core/src/test/java/org/elasticsearch/plugins/MetaPluginInfoTests.java new file mode 100644 index 00000000000..2b7f50056a9 --- /dev/null +++ b/core/src/test/java/org/elasticsearch/plugins/MetaPluginInfoTests.java @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.plugins; + +import org.apache.lucene.util.LuceneTestCase; +import org.elasticsearch.Version; +import org.elasticsearch.test.ESTestCase; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; + +@LuceneTestCase.SuppressFileSystems(value = "ExtrasFS") +public class MetaPluginInfoTests extends ESTestCase { + + public void testReadFromProperties() throws Exception { + Path pluginDir = createTempDir().resolve("fake-meta-plugin"); + PluginTestUtil.writeMetaPluginProperties(pluginDir, + "description", "fake desc", + "name", "my_meta_plugin"); + MetaPluginInfo info = MetaPluginInfo.readFromProperties(pluginDir); + assertEquals("my_meta_plugin", info.getName()); + assertEquals("fake desc", info.getDescription()); + } + + public void testReadFromPropertiesNameMissing() throws Exception { + Path pluginDir = createTempDir().resolve("fake-meta-plugin"); + PluginTestUtil.writeMetaPluginProperties(pluginDir); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> MetaPluginInfo.readFromProperties(pluginDir)); + assertThat(e.getMessage(), containsString("property [name] is missing for")); + + PluginTestUtil.writeMetaPluginProperties(pluginDir, "name", ""); + e = expectThrows(IllegalArgumentException.class, () -> MetaPluginInfo.readFromProperties(pluginDir)); + assertThat(e.getMessage(), containsString("property [name] is missing for")); + } + + public void testReadFromPropertiesDescriptionMissing() throws Exception { + Path pluginDir = createTempDir().resolve("fake-meta-plugin"); + PluginTestUtil.writeMetaPluginProperties(pluginDir, "name", "fake-meta-plugin"); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> MetaPluginInfo.readFromProperties(pluginDir)); + assertThat(e.getMessage(), containsString("[description] is missing")); + } + + public void testUnknownProperties() throws Exception { + Path pluginDir = createTempDir().resolve("fake-meta-plugin"); + PluginTestUtil.writeMetaPluginProperties(pluginDir, + "extra", "property", + "unknown", "property", + "description", "fake desc", + "name", "my_meta_plugin"); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> MetaPluginInfo.readFromProperties(pluginDir)); + assertThat(e.getMessage(), containsString("Unknown properties in meta plugin descriptor")); + } + + public void testExtractAllPluginsWithDuplicates() throws Exception { + Path pluginDir = createTempDir().resolve("plugins"); + // Simple plugin + Path plugin1 = pluginDir.resolve("plugin1"); + Files.createDirectories(plugin1); + PluginTestUtil.writePluginProperties(plugin1, + "description", "fake desc", + "name", "plugin1", + "version", "1.0", + "elasticsearch.version", Version.CURRENT.toString(), + "java.version", System.getProperty("java.specification.version"), + "classname", "FakePlugin"); + + // Meta plugin + Path metaPlugin = pluginDir.resolve("meta_plugin"); + Files.createDirectory(metaPlugin); + PluginTestUtil.writeMetaPluginProperties(metaPlugin, + "description", "fake desc", + "name", "meta_plugin"); + Path plugin2 = metaPlugin.resolve("plugin1"); + Files.createDirectory(plugin2); + PluginTestUtil.writePluginProperties(plugin2, + "description", "fake desc", + "name", "plugin1", + "version", "1.0", + "elasticsearch.version", Version.CURRENT.toString(), + "java.version", System.getProperty("java.specification.version"), + "classname", "FakePlugin"); + Path plugin3 = metaPlugin.resolve("plugin2"); + Files.createDirectory(plugin3); + PluginTestUtil.writePluginProperties(plugin3, + "description", "fake desc", + "name", "plugin2", + "version", "1.0", + "elasticsearch.version", Version.CURRENT.toString(), + "java.version", System.getProperty("java.specification.version"), + "classname", "FakePlugin"); + + IllegalStateException exc = + expectThrows(IllegalStateException.class, () -> PluginInfo.extractAllPlugins(pluginDir)); + assertThat(exc.getMessage(), containsString("duplicate plugin")); + assertThat(exc.getMessage(), endsWith("plugin1")); + } +} diff --git a/core/src/test/java/org/elasticsearch/plugins/PluginInfoTests.java b/core/src/test/java/org/elasticsearch/plugins/PluginInfoTests.java index 6d2b09f87eb..a767dad204e 100644 --- a/core/src/test/java/org/elasticsearch/plugins/PluginInfoTests.java +++ b/core/src/test/java/org/elasticsearch/plugins/PluginInfoTests.java @@ -41,7 +41,7 @@ public class PluginInfoTests extends ESTestCase { public void testReadFromProperties() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc", "name", "my_plugin", "version", "1.0", @@ -58,25 +58,25 @@ public class PluginInfoTests extends ESTestCase { public void testReadFromPropertiesNameMissing() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir); + PluginTestUtil.writePluginProperties(pluginDir); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> PluginInfo.readFromProperties(pluginDir)); assertThat(e.getMessage(), containsString("property [name] is missing in")); - PluginTestUtil.writeProperties(pluginDir, "name", ""); + PluginTestUtil.writePluginProperties(pluginDir, "name", ""); e = expectThrows(IllegalArgumentException.class, () -> PluginInfo.readFromProperties(pluginDir)); assertThat(e.getMessage(), containsString("property [name] is missing in")); } public void testReadFromPropertiesDescriptionMissing() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, "name", "fake-plugin"); + PluginTestUtil.writePluginProperties(pluginDir, "name", "fake-plugin"); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> PluginInfo.readFromProperties(pluginDir)); assertThat(e.getMessage(), containsString("[description] is missing")); } public void testReadFromPropertiesVersionMissing() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties( + PluginTestUtil.writePluginProperties( pluginDir, "description", "fake desc", "name", "fake-plugin"); IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> PluginInfo.readFromProperties(pluginDir)); assertThat(e.getMessage(), containsString("[version] is missing")); @@ -84,7 +84,7 @@ public class PluginInfoTests extends ESTestCase { public void testReadFromPropertiesElasticsearchVersionMissing() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc", "name", "my_plugin", "version", "1.0"); @@ -94,7 +94,7 @@ public class PluginInfoTests extends ESTestCase { public void testReadFromPropertiesJavaVersionMissing() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc", "name", "my_plugin", "elasticsearch.version", Version.CURRENT.toString(), @@ -106,7 +106,7 @@ public class PluginInfoTests extends ESTestCase { public void testReadFromPropertiesJavaVersionIncompatible() throws Exception { String pluginName = "fake-plugin"; Path pluginDir = createTempDir().resolve(pluginName); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc", "name", pluginName, "elasticsearch.version", Version.CURRENT.toString(), @@ -120,7 +120,7 @@ public class PluginInfoTests extends ESTestCase { public void testReadFromPropertiesBadJavaVersionFormat() throws Exception { String pluginName = "fake-plugin"; Path pluginDir = createTempDir().resolve(pluginName); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc", "name", pluginName, "elasticsearch.version", Version.CURRENT.toString(), @@ -134,7 +134,7 @@ public class PluginInfoTests extends ESTestCase { public void testReadFromPropertiesBogusElasticsearchVersion() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc", "version", "1.0", "name", "my_plugin", @@ -145,7 +145,7 @@ public class PluginInfoTests extends ESTestCase { public void testReadFromPropertiesOldElasticsearchVersion() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc", "name", "my_plugin", "version", "1.0", @@ -156,7 +156,7 @@ public class PluginInfoTests extends ESTestCase { public void testReadFromPropertiesJvmMissingClassname() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc", "name", "my_plugin", "version", "1.0", @@ -168,7 +168,7 @@ public class PluginInfoTests extends ESTestCase { public void testExtendedPluginsSingleExtension() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc", "name", "my_plugin", "version", "1.0", @@ -182,7 +182,7 @@ public class PluginInfoTests extends ESTestCase { public void testExtendedPluginsMultipleExtensions() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc", "name", "my_plugin", "version", "1.0", @@ -196,7 +196,7 @@ public class PluginInfoTests extends ESTestCase { public void testExtendedPluginsEmpty() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc", "name", "my_plugin", "version", "1.0", @@ -224,7 +224,7 @@ public class PluginInfoTests extends ESTestCase { List plugins = new ArrayList<>(); plugins.add(new PluginInfo("c", "foo", "dummy", "dummyclass", Collections.emptyList(), randomBoolean(), randomBoolean())); plugins.add(new PluginInfo("b", "foo", "dummy", "dummyclass", Collections.emptyList(),randomBoolean(), randomBoolean())); - plugins.add(new PluginInfo("e", "foo", "dummy", "dummyclass", Collections.emptyList(),randomBoolean(), randomBoolean())); + plugins.add(new PluginInfo( "e", "foo", "dummy", "dummyclass", Collections.emptyList(),randomBoolean(), randomBoolean())); plugins.add(new PluginInfo("a", "foo", "dummy", "dummyclass", Collections.emptyList(),randomBoolean(), randomBoolean())); plugins.add(new PluginInfo("d", "foo", "dummy", "dummyclass", Collections.emptyList(),randomBoolean(), randomBoolean())); PluginsAndModules pluginsInfo = new PluginsAndModules(plugins, Collections.emptyList()); @@ -236,7 +236,7 @@ public class PluginInfoTests extends ESTestCase { public void testUnknownProperties() throws Exception { Path pluginDir = createTempDir().resolve("fake-plugin"); - PluginTestUtil.writeProperties(pluginDir, + PluginTestUtil.writePluginProperties(pluginDir, "extra", "property", "unknown", "property", "description", "fake desc", diff --git a/core/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java b/core/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java index 16c3eb34b0e..1bb9d675988 100644 --- a/core/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java +++ b/core/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java @@ -180,7 +180,7 @@ public class PluginsServiceTests extends ESTestCase { Files.createFile(fake.resolve("plugin.jar")); final Path removing = home.resolve("plugins").resolve(".removing-fake"); Files.createFile(removing); - PluginTestUtil.writeProperties( + PluginTestUtil.writePluginProperties( fake, "description", "fake", "name", "fake", @@ -541,7 +541,7 @@ public class PluginsServiceTests extends ESTestCase { Settings settings = Settings.builder().put(Environment.PATH_HOME_SETTING.getKey(), homeDir).build(); Path pluginsDir = homeDir.resolve("plugins"); Path mypluginDir = pluginsDir.resolve("myplugin"); - PluginTestUtil.writeProperties( + PluginTestUtil.writePluginProperties( mypluginDir, "description", "whatever", "name", "myplugin", @@ -554,7 +554,7 @@ public class PluginsServiceTests extends ESTestCase { Files.copy(jar, mypluginDir.resolve("plugin.jar")); } Path nonextensibleDir = pluginsDir.resolve("nonextensible"); - PluginTestUtil.writeProperties( + PluginTestUtil.writePluginProperties( nonextensibleDir, "description", "whatever", "name", "nonextensible", diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java index 5fedb050ff2..216eb46411a 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/InstallPluginCommand.java @@ -30,6 +30,7 @@ import org.elasticsearch.cli.EnvironmentAwareCommand; import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.UserException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.hash.MessageDigests; @@ -86,8 +87,8 @@ import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE; *
  • A URL to a plugin zip
  • * * - * Plugins are packaged as zip files. Each packaged plugin must contain a - * plugin properties file. See {@link PluginInfo}. + * Plugins are packaged as zip files. Each packaged plugin must contain a plugin properties file + * or a meta plugin properties file. See {@link PluginInfo} and {@link MetaPluginInfo}, respectively. *

    * The installation process first extracts the plugin files into a temporary * directory in order to verify the plugin satisfies the following requirements: @@ -105,6 +106,11 @@ import static org.elasticsearch.cli.Terminal.Verbosity.VERBOSE; * files specific to the plugin. The config files be installed into a subdirectory of the * elasticsearch config directory, using the name of the plugin. If any files to be installed * already exist, they will be skipped. + *

    + * If the plugin is a meta plugin, the installation process installs each plugin separately + * inside the meta plugin directory. The {@code bin} and {@code config} directory are also moved + * inside the meta plugin directory. + *

    */ class InstallPluginCommand extends EnvironmentAwareCommand { @@ -527,22 +533,44 @@ class InstallPluginCommand extends EnvironmentAwareCommand { return Files.createTempDirectory(pluginsDir, ".installing-"); } - /** Load information about the plugin, and verify it can be installed with no errors. */ - private PluginInfo verify(Terminal terminal, Path pluginRoot, boolean isBatch, Environment env) throws Exception { - // read and validate the plugin descriptor - PluginInfo info = PluginInfo.readFromProperties(pluginRoot); - - // checking for existing version of the plugin - final Path destination = env.pluginsFile().resolve(info.getName()); + // checking for existing version of the plugin + private void verifyPluginName(Path pluginPath, String pluginName, Path candidateDir) throws UserException, IOException { + final Path destination = pluginPath.resolve(pluginName); if (Files.exists(destination)) { final String message = String.format( Locale.ROOT, "plugin directory [%s] already exists; if you need to update the plugin, " + "uninstall it first using command 'remove %s'", destination.toAbsolutePath(), - info.getName()); + pluginName); throw new UserException(PLUGIN_EXISTS, message); } + // checks meta plugins too + try (DirectoryStream stream = Files.newDirectoryStream(pluginPath)) { + for (Path plugin : stream) { + if (candidateDir.equals(plugin.resolve(pluginName))) { + continue; + } + if (MetaPluginInfo.isMetaPlugin(plugin) && Files.exists(plugin.resolve(pluginName))) { + final MetaPluginInfo info = MetaPluginInfo.readFromProperties(plugin); + final String message = String.format( + Locale.ROOT, + "plugin name [%s] already exists in a meta plugin; if you need to update the meta plugin, " + + "uninstall it first using command 'remove %s'", + plugin.resolve(pluginName).toAbsolutePath(), + info.getName()); + throw new UserException(PLUGIN_EXISTS, message); + } + } + } + } + + /** Load information about the plugin, and verify it can be installed with no errors. */ + private PluginInfo verify(Terminal terminal, Path pluginRoot, boolean isBatch, Environment env) throws Exception { + final PluginInfo info = PluginInfo.readFromProperties(pluginRoot); + + // checking for existing version of the plugin + verifyPluginName(env.pluginsFile(), info.getName(), pluginRoot); PluginsService.checkForFailedPluginRemovals(env.pluginsFile()); @@ -569,14 +597,15 @@ class InstallPluginCommand extends EnvironmentAwareCommand { } /** check a candidate plugin for jar hell before installing it */ - void jarHellCheck(PluginInfo info, Path candidate, Path pluginsDir, Path modulesDir) throws Exception { + void jarHellCheck(PluginInfo candidateInfo, Path candidateDir, Path pluginsDir, Path modulesDir) throws Exception { // create list of current jars in classpath final Set jars = new HashSet<>(JarHell.parseClassPath()); + // read existing bundles. this does some checks on the installation too. Set bundles = new HashSet<>(PluginsService.getPluginBundles(pluginsDir)); bundles.addAll(PluginsService.getModuleBundles(modulesDir)); - bundles.add(new PluginsService.Bundle(info, candidate)); + bundles.add(new PluginsService.Bundle(candidateInfo, candidateDir)); List sortedBundles = PluginsService.sortBundles(bundles); // check jarhell of all plugins so we know this plugin and anything depending on it are ok together @@ -590,62 +619,15 @@ class InstallPluginCommand extends EnvironmentAwareCommand { // TODO: verify the classname exists in one of the jars! } - /** - * Installs the plugin from {@code tmpRoot} into the plugins dir. - * If the plugin has a bin dir and/or a config dir, those are copied. - */ private void install(Terminal terminal, boolean isBatch, Path tmpRoot, Environment env) throws Exception { List deleteOnFailure = new ArrayList<>(); deleteOnFailure.add(tmpRoot); - try { - PluginInfo info = verify(terminal, tmpRoot, isBatch, env); - final Path destination = env.pluginsFile().resolve(info.getName()); - - Path tmpBinDir = tmpRoot.resolve("bin"); - if (Files.exists(tmpBinDir)) { - Path destBinDir = env.binFile().resolve(info.getName()); - deleteOnFailure.add(destBinDir); - installBin(info, tmpBinDir, destBinDir); + if (MetaPluginInfo.isMetaPlugin(tmpRoot)) { + installMetaPlugin(terminal, isBatch, tmpRoot, env, deleteOnFailure); + } else { + installPlugin(terminal, isBatch, tmpRoot, env, deleteOnFailure); } - - Path tmpConfigDir = tmpRoot.resolve("config"); - if (Files.exists(tmpConfigDir)) { - // some files may already exist, and we don't remove plugin config files on plugin removal, - // so any installed config files are left on failure too - installConfig(info, tmpConfigDir, env.configFile().resolve(info.getName())); - } - - Files.move(tmpRoot, destination, StandardCopyOption.ATOMIC_MOVE); - Files.walkFileTree(destination, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { - if ("bin".equals(file.getParent().getFileName().toString())) { - setFileAttributes(file, BIN_FILES_PERMS); - } else { - setFileAttributes(file, PLUGIN_FILES_PERMS); - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException { - setFileAttributes(dir, PLUGIN_DIR_PERMS); - return FileVisitResult.CONTINUE; - } - }); - - if (info.requiresKeystore()) { - KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile()); - if (keystore == null) { - terminal.println("Elasticsearch keystore is required by plugin [" + info.getName() + "], creating..."); - keystore = KeyStoreWrapper.create(new char[0]); - keystore.save(env.configFile()); - } - } - - terminal.println("-> Installed " + info.getName()); - } catch (Exception installProblem) { try { IOUtils.rm(deleteOnFailure.toArray(new Path[0])); @@ -656,12 +638,119 @@ class InstallPluginCommand extends EnvironmentAwareCommand { } } + /** + * Installs the meta plugin and all the bundled plugins from {@code tmpRoot} into the plugins dir. + * If a bundled plugin has a bin dir and/or a config dir, those are copied. + */ + private void installMetaPlugin(Terminal terminal, boolean isBatch, Path tmpRoot, + Environment env, List deleteOnFailure) throws Exception { + final MetaPluginInfo metaInfo = MetaPluginInfo.readFromProperties(tmpRoot); + verifyPluginName(env.pluginsFile(), metaInfo.getName(), tmpRoot); + final Path destination = env.pluginsFile().resolve(metaInfo.getName()); + deleteOnFailure.add(destination); + terminal.println(VERBOSE, metaInfo.toString()); + final List pluginPaths = new ArrayList<>(); + try (DirectoryStream paths = Files.newDirectoryStream(tmpRoot)) { + // Extract bundled plugins path and validate plugin names + for (Path plugin : paths) { + if (MetaPluginInfo.isPropertiesFile(plugin)) { + continue; + } + final PluginInfo info = PluginInfo.readFromProperties(plugin); + verifyPluginName(env.pluginsFile(), info.getName(), plugin); + pluginPaths.add(plugin); + } + } + final List pluginInfos = new ArrayList<>(); + for (Path plugin : pluginPaths) { + final PluginInfo info = verify(terminal, plugin, isBatch, env); + pluginInfos.add(info); + Path tmpBinDir = plugin.resolve("bin"); + if (Files.exists(tmpBinDir)) { + Path destBinDir = env.binFile().resolve(metaInfo.getName()).resolve(info.getName()); + deleteOnFailure.add(destBinDir); + installBin(info, tmpBinDir, destBinDir); + } + + Path tmpConfigDir = plugin.resolve("config"); + if (Files.exists(tmpConfigDir)) { + // some files may already exist, and we don't remove plugin config files on plugin removal, + // so any installed config files are left on failure too + Path destConfigDir = env.configFile().resolve(metaInfo.getName()).resolve(info.getName()); + installConfig(info, tmpConfigDir, destConfigDir); + } + } + movePlugin(tmpRoot, destination); + for (PluginInfo info : pluginInfos) { + if (info.requiresKeystore()) { + createKeystoreIfNeeded(terminal, env, info); + break; + } + } + String[] plugins = pluginInfos.stream().map(PluginInfo::getName).toArray(String[]::new); + terminal.println("-> Installed " + metaInfo.getName() + " with: " + Strings.arrayToCommaDelimitedString(plugins)); + } + + /** + * Installs the plugin from {@code tmpRoot} into the plugins dir. + * If the plugin has a bin dir and/or a config dir, those are copied. + */ + private void installPlugin(Terminal terminal, boolean isBatch, Path tmpRoot, + Environment env, List deleteOnFailure) throws Exception { + final PluginInfo info = verify(terminal, tmpRoot, isBatch, env); + final Path destination = env.pluginsFile().resolve(info.getName()); + deleteOnFailure.add(destination); + + Path tmpBinDir = tmpRoot.resolve("bin"); + if (Files.exists(tmpBinDir)) { + Path destBinDir = env.binFile().resolve(info.getName()); + deleteOnFailure.add(destBinDir); + installBin(info, tmpBinDir, destBinDir); + } + + Path tmpConfigDir = tmpRoot.resolve("config"); + if (Files.exists(tmpConfigDir)) { + // some files may already exist, and we don't remove plugin config files on plugin removal, + // so any installed config files are left on failure too + Path destConfigDir = env.configFile().resolve(info.getName()); + installConfig(info, tmpConfigDir, destConfigDir); + } + movePlugin(tmpRoot, destination); + if (info.requiresKeystore()) { + createKeystoreIfNeeded(terminal, env, info); + } + terminal.println("-> Installed " + info.getName()); + } + + /** Moves the plugin directory into its final destination. **/ + private void movePlugin(Path tmpRoot, Path destination) throws IOException { + Files.move(tmpRoot, destination, StandardCopyOption.ATOMIC_MOVE); + Files.walkFileTree(destination, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException { + if ("bin".equals(file.getParent().getFileName().toString())) { + setFileAttributes(file, BIN_FILES_PERMS); + } else { + setFileAttributes(file, PLUGIN_FILES_PERMS); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException { + setFileAttributes(dir, PLUGIN_DIR_PERMS); + return FileVisitResult.CONTINUE; + } + }); + } + + /** Copies the files from {@code tmpBinDir} into {@code destBinDir}, along with permissions from dest dirs parent. */ private void installBin(PluginInfo info, Path tmpBinDir, Path destBinDir) throws Exception { if (Files.isDirectory(tmpBinDir) == false) { throw new UserException(PLUGIN_MALFORMED, "bin in plugin " + info.getName() + " is not a directory"); } - Files.createDirectory(destBinDir); + Files.createDirectories(destBinDir); setFileAttributes(destBinDir, BIN_DIR_PERMS); try (DirectoryStream stream = Files.newDirectoryStream(tmpBinDir)) { @@ -719,6 +808,15 @@ class InstallPluginCommand extends EnvironmentAwareCommand { IOUtils.rm(tmpConfigDir); // clean up what we just copied } + private void createKeystoreIfNeeded(Terminal terminal, Environment env, PluginInfo info) throws Exception { + KeyStoreWrapper keystore = KeyStoreWrapper.load(env.configFile()); + if (keystore == null) { + terminal.println("Elasticsearch keystore is required by plugin [" + info.getName() + "], creating..."); + keystore = KeyStoreWrapper.create(new char[0]); + keystore.save(env.configFile()); + } + } + private static void setOwnerGroup(final Path path, final PosixFileAttributes attributes) throws IOException { Objects.requireNonNull(attributes); PosixFileAttributeView fileAttributeView = Files.getFileAttributeView(path, PosixFileAttributeView.class); diff --git a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java index c2b5ce34b54..70acf62bd8e 100644 --- a/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java +++ b/distribution/tools/plugin-cli/src/main/java/org/elasticsearch/plugins/ListPluginsCommand.java @@ -22,6 +22,7 @@ package org.elasticsearch.plugins; import joptsimple.OptionSet; import org.elasticsearch.cli.EnvironmentAwareCommand; import org.elasticsearch.cli.Terminal; +import org.elasticsearch.common.Nullable; import org.elasticsearch.env.Environment; import java.io.IOException; @@ -29,8 +30,11 @@ import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; /** * A command for the plugin cli to list plugins installed in elasticsearch. @@ -56,16 +60,38 @@ class ListPluginsCommand extends EnvironmentAwareCommand { } Collections.sort(plugins); for (final Path plugin : plugins) { - terminal.println(Terminal.Verbosity.SILENT, plugin.getFileName().toString()); - try { - PluginInfo info = PluginInfo.readFromProperties(env.pluginsFile().resolve(plugin.toAbsolutePath())); - terminal.println(Terminal.Verbosity.VERBOSE, info.toString()); - } catch (IllegalArgumentException e) { - if (e.getMessage().contains("incompatible with version")) { - terminal.println("WARNING: " + e.getMessage()); - } else { - throw e; + if (MetaPluginInfo.isMetaPlugin(plugin)) { + MetaPluginInfo metaInfo = MetaPluginInfo.readFromProperties(plugin); + List subPluginPaths = new ArrayList<>(); + try (DirectoryStream subPaths = Files.newDirectoryStream(plugin)) { + for (Path subPlugin : subPaths) { + if (MetaPluginInfo.isPropertiesFile(subPlugin)) { + continue; + } + subPluginPaths.add(subPlugin); + } } + Collections.sort(subPluginPaths); + terminal.println(Terminal.Verbosity.SILENT, metaInfo.getName()); + for (Path subPlugin : subPluginPaths) { + printPlugin(env, terminal, subPlugin, "\t"); + } + } else { + printPlugin(env, terminal, plugin, ""); + } + } + } + + private void printPlugin(Environment env, Terminal terminal, Path plugin, String prefix) throws IOException { + terminal.println(Terminal.Verbosity.SILENT, prefix + plugin.getFileName().toString()); + try { + PluginInfo info = PluginInfo.readFromProperties(env.pluginsFile().resolve(plugin.toAbsolutePath())); + terminal.println(Terminal.Verbosity.VERBOSE, info.toString(prefix)); + } catch (IllegalArgumentException e) { + if (e.getMessage().contains("incompatible with version")) { + terminal.println("WARNING: " + e.getMessage()); + } else { + throw e; } } } diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java index e545609ccbf..9d05fe1e766 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/InstallPluginCommandTests.java @@ -115,7 +115,7 @@ public class InstallPluginCommandTests extends ESTestCase { super.setUp(); skipJarHellCommand = new InstallPluginCommand() { @Override - void jarHellCheck(PluginInfo info, Path candidate, Path pluginsDir, Path modulesDir) throws Exception { + void jarHellCheck(PluginInfo candidateInfo, Path candidate, Path pluginsDir, Path modulesDir) throws Exception { // no jarhell check } }; @@ -214,7 +214,19 @@ public class InstallPluginCommandTests extends ESTestCase { return createPlugin(name, structure, false, additionalProps).toUri().toURL().toString(); } - static Path createPlugin(String name, Path structure, boolean createSecurityPolicyFile, String... additionalProps) throws IOException { + /** creates an meta plugin .zip and returns the url for testing */ + static String createMetaPluginUrl(String name, Path structure) throws IOException { + return createMetaPlugin(name, structure).toUri().toURL().toString(); + } + + static void writeMetaPlugin(String name, Path structure) throws IOException { + PluginTestUtil.writeMetaPluginProperties(structure, + "description", "fake desc", + "name", name + ); + } + + static void writePlugin(String name, Path structure, boolean createSecurityPolicyFile, String... additionalProps) throws IOException { String[] properties = Stream.concat(Stream.of( "description", "fake desc", "name", name, @@ -223,12 +235,22 @@ public class InstallPluginCommandTests extends ESTestCase { "java.version", System.getProperty("java.specification.version"), "classname", "FakePlugin" ), Arrays.stream(additionalProps)).toArray(String[]::new); - PluginTestUtil.writeProperties(structure, properties); + PluginTestUtil.writePluginProperties(structure, properties); if (createSecurityPolicyFile) { String securityPolicyContent = "grant {\n permission java.lang.RuntimePermission \"setFactory\";\n};\n"; Files.write(structure.resolve("plugin-security.policy"), securityPolicyContent.getBytes(StandardCharsets.UTF_8)); } - writeJar(structure.resolve("plugin.jar"), "FakePlugin"); + String className = name.substring(0, 1).toUpperCase(Locale.ENGLISH) + name.substring(1) + "Plugin"; + writeJar(structure.resolve("plugin.jar"), className); + } + + static Path createPlugin(String name, Path structure, boolean createSecurityPolicyFile, String... additionalProps) throws IOException { + writePlugin(name, structure, createSecurityPolicyFile, additionalProps); + return writeZip(structure, "elasticsearch"); + } + + static Path createMetaPlugin(String name, Path structure) throws IOException { + writeMetaPlugin(name, structure); return writeZip(structure, "elasticsearch"); } @@ -243,8 +265,20 @@ public class InstallPluginCommandTests extends ESTestCase { return terminal; } + void assertMetaPlugin(String metaPlugin, String name, Path original, Environment env) throws IOException { + assertPluginInternal(name, original, env, + env.pluginsFile().resolve(metaPlugin), env.configFile().resolve(metaPlugin), env.binFile().resolve(metaPlugin)); + } + + void assertPlugin(String name, Path original, Environment env) throws IOException { - Path got = env.pluginsFile().resolve(name); + assertPluginInternal(name, original, env, + env.pluginsFile(), env.configFile(), env.binFile()); + } + + void assertPluginInternal(String name, Path original, Environment env, + Path pluginsFile, Path configFile, Path binFile) throws IOException { + Path got = pluginsFile.resolve(name); assertTrue("dir " + name + " exists", Files.exists(got)); if (isPosix) { @@ -265,12 +299,12 @@ public class InstallPluginCommandTests extends ESTestCase { assertFalse("bin was not copied", Files.exists(got.resolve("bin"))); assertFalse("config was not copied", Files.exists(got.resolve("config"))); if (Files.exists(original.resolve("bin"))) { - Path binDir = env.binFile().resolve(name); + Path binDir = binFile.resolve(name); assertTrue("bin dir exists", Files.exists(binDir)); assertTrue("bin is a dir", Files.isDirectory(binDir)); PosixFileAttributes binAttributes = null; if (isPosix) { - binAttributes = Files.readAttributes(env.binFile(), PosixFileAttributes.class); + binAttributes = Files.readAttributes(binFile, PosixFileAttributes.class); } try (DirectoryStream stream = Files.newDirectoryStream(binDir)) { for (Path file : stream) { @@ -283,7 +317,7 @@ public class InstallPluginCommandTests extends ESTestCase { } } if (Files.exists(original.resolve("config"))) { - Path configDir = env.configFile().resolve(name); + Path configDir = configFile.resolve(name); assertTrue("config dir exists", Files.exists(configDir)); assertTrue("config is a dir", Files.isDirectory(configDir)); @@ -292,7 +326,7 @@ public class InstallPluginCommandTests extends ESTestCase { if (isPosix) { PosixFileAttributes configAttributes = - Files.getFileAttributeView(env.configFile(), PosixFileAttributeView.class).readAttributes(); + Files.getFileAttributeView(configFile, PosixFileAttributeView.class).readAttributes(); user = configAttributes.owner(); group = configAttributes.group(); @@ -344,9 +378,23 @@ public class InstallPluginCommandTests extends ESTestCase { assertPlugin("fake", pluginDir, env.v2()); } - public void testInstallFailsIfPreviouslyRemovedPluginFailed() throws Exception { + public void testWithMetaPlugin() throws Exception { Tuple env = createEnv(fs, temp); Path pluginDir = createPluginDir(temp); + Files.createDirectory(pluginDir.resolve("fake1")); + writePlugin("fake1", pluginDir.resolve("fake1"), false); + Files.createDirectory(pluginDir.resolve("fake2")); + writePlugin("fake2", pluginDir.resolve("fake2"), false); + String pluginZip = createMetaPluginUrl("my_plugins", pluginDir); + installPlugin(pluginZip, env.v1()); + assertMetaPlugin("my_plugins", "fake1", pluginDir, env.v2()); + assertMetaPlugin("my_plugins", "fake2", pluginDir, env.v2()); + } + + public void testInstallFailsIfPreviouslyRemovedPluginFailed() throws Exception { + Tuple env = createEnv(fs, temp); + Path metaDir = createPluginDir(temp); + Path pluginDir = metaDir.resolve("fake"); String pluginZip = createPluginUrl("fake", pluginDir); final Path removing = env.v2().pluginsFile().resolve(".removing-failed"); Files.createDirectory(removing); @@ -356,6 +404,11 @@ public class InstallPluginCommandTests extends ESTestCase { "found file [%s] from a failed attempt to remove the plugin [failed]; execute [elasticsearch-plugin remove failed]", removing); assertThat(e, hasToString(containsString(expected))); + + // test with meta plugin + String metaZip = createMetaPluginUrl("my_plugins", metaDir); + final IllegalStateException e1 = expectThrows(IllegalStateException.class, () -> installPlugin(metaZip, env.v1())); + assertThat(e1, hasToString(containsString(expected))); } public void testSpaceInUrl() throws Exception { @@ -418,6 +471,23 @@ public class InstallPluginCommandTests extends ESTestCase { assertInstallCleaned(environment.v2()); } + public void testJarHellInMetaPlugin() throws Exception { + // jar hell test needs a real filesystem + assumeTrue("real filesystem", isReal); + Tuple environment = createEnv(fs, temp); + Path pluginDir = createPluginDir(temp); + Files.createDirectory(pluginDir.resolve("fake1")); + writePlugin("fake1", pluginDir.resolve("fake1"), false); + Files.createDirectory(pluginDir.resolve("fake2")); + writePlugin("fake2", pluginDir.resolve("fake2"), false); // adds plugin.jar with Fake2Plugin + writeJar(pluginDir.resolve("fake2").resolve("other.jar"), "Fake2Plugin"); + String pluginZip = createMetaPluginUrl("my_plugins", pluginDir); + IllegalStateException e = expectThrows(IllegalStateException.class, + () -> installPlugin(pluginZip, environment.v1(), defaultCommand)); + assertTrue(e.getMessage(), e.getMessage().contains("jar hell")); + assertInstallCleaned(environment.v2()); + } + public void testIsolatedPlugins() throws Exception { Tuple env = createEnv(fs, temp); // these both share the same FakePlugin class @@ -441,6 +511,23 @@ public class InstallPluginCommandTests extends ESTestCase { assertInstallCleaned(env.v2()); } + public void testExistingMetaPlugin() throws Exception { + Tuple env = createEnv(fs, temp); + Path metaZip = createPluginDir(temp); + Path pluginDir = metaZip.resolve("fake"); + Files.createDirectory(pluginDir); + String pluginZip = createPluginUrl("fake", pluginDir); + installPlugin(pluginZip, env.v1()); + UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip, env.v1())); + assertTrue(e.getMessage(), e.getMessage().contains("already exists")); + assertInstallCleaned(env.v2()); + + String anotherZip = createMetaPluginUrl("another_plugins", metaZip); + e = expectThrows(UserException.class, () -> installPlugin(anotherZip, env.v1())); + assertTrue(e.getMessage(), e.getMessage().contains("already exists")); + assertInstallCleaned(env.v2()); + } + public void testBin() throws Exception { Tuple env = createEnv(fs, temp); Path pluginDir = createPluginDir(temp); @@ -452,20 +539,43 @@ public class InstallPluginCommandTests extends ESTestCase { assertPlugin("fake", pluginDir, env.v2()); } + public void testMetaBin() throws Exception { + Tuple env = createEnv(fs, temp); + Path metaDir = createPluginDir(temp); + Path pluginDir = metaDir.resolve("fake"); + Files.createDirectory(pluginDir); + writePlugin("fake", pluginDir, false); + Path binDir = pluginDir.resolve("bin"); + Files.createDirectory(binDir); + Files.createFile(binDir.resolve("somescript")); + String pluginZip = createMetaPluginUrl("my_plugins", metaDir); + installPlugin(pluginZip, env.v1()); + assertMetaPlugin("my_plugins","fake", pluginDir, env.v2()); + } + public void testBinNotDir() throws Exception { Tuple env = createEnv(fs, temp); - Path pluginDir = createPluginDir(temp); + Path metaDir = createPluginDir(temp); + Path pluginDir = metaDir.resolve("fake"); + Files.createDirectory(pluginDir); Path binDir = pluginDir.resolve("bin"); Files.createFile(binDir); String pluginZip = createPluginUrl("fake", pluginDir); UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip, env.v1())); assertTrue(e.getMessage(), e.getMessage().contains("not a directory")); assertInstallCleaned(env.v2()); + + String metaZip = createMetaPluginUrl("my_plugins", metaDir); + e = expectThrows(UserException.class, () -> installPlugin(metaZip, env.v1())); + assertTrue(e.getMessage(), e.getMessage().contains("not a directory")); + assertInstallCleaned(env.v2()); } public void testBinContainsDir() throws Exception { Tuple env = createEnv(fs, temp); - Path pluginDir = createPluginDir(temp); + Path metaDir = createPluginDir(temp); + Path pluginDir = metaDir.resolve("fake"); + Files.createDirectory(pluginDir); Path dirInBinDir = pluginDir.resolve("bin").resolve("foo"); Files.createDirectories(dirInBinDir); Files.createFile(dirInBinDir.resolve("somescript")); @@ -473,11 +583,16 @@ public class InstallPluginCommandTests extends ESTestCase { UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip, env.v1())); assertTrue(e.getMessage(), e.getMessage().contains("Directories not allowed in bin dir for plugin")); assertInstallCleaned(env.v2()); + + String metaZip = createMetaPluginUrl("my_plugins", metaDir); + e = expectThrows(UserException.class, () -> installPlugin(metaZip, env.v1())); + assertTrue(e.getMessage(), e.getMessage().contains("Directories not allowed in bin dir for plugin")); + assertInstallCleaned(env.v2()); } public void testBinConflict() throws Exception { Tuple env = createEnv(fs, temp); - Path pluginDir = createPluginDir(temp); + Path pluginDir = createPluginDir(temp); Path binDir = pluginDir.resolve("bin"); Files.createDirectory(binDir); Files.createFile(binDir.resolve("somescript")); @@ -505,6 +620,27 @@ public class InstallPluginCommandTests extends ESTestCase { } } + public void testMetaBinPermissions() throws Exception { + assumeTrue("posix filesystem", isPosix); + Tuple env = createEnv(fs, temp); + Path metaDir = createPluginDir(temp); + Path pluginDir = metaDir.resolve("fake"); + Files.createDirectory(pluginDir); + writePlugin("fake", pluginDir, false); + Path binDir = pluginDir.resolve("bin"); + Files.createDirectory(binDir); + Files.createFile(binDir.resolve("somescript")); + String pluginZip = createMetaPluginUrl("my_plugins", metaDir); + try (PosixPermissionsResetter binAttrs = new PosixPermissionsResetter(env.v2().binFile())) { + Set perms = binAttrs.getCopyPermissions(); + // make sure at least one execute perm is missing, so we know we forced it during installation + perms.remove(PosixFilePermission.GROUP_EXECUTE); + binAttrs.setPermissions(perms); + installPlugin(pluginZip, env.v1()); + assertMetaPlugin("my_plugins", "fake", pluginDir, env.v2()); + } + } + public void testPluginPermissions() throws Exception { assumeTrue("posix filesystem", isPosix); @@ -596,15 +732,44 @@ public class InstallPluginCommandTests extends ESTestCase { assertTrue(Files.exists(envConfigDir.resolve("other.yml"))); } + public void testExistingMetaConfig() throws Exception { + Tuple env = createEnv(fs, temp); + Path envConfigDir = env.v2().configFile().resolve("my_plugins").resolve("fake"); + Files.createDirectories(envConfigDir); + Files.write(envConfigDir.resolve("custom.yml"), "existing config".getBytes(StandardCharsets.UTF_8)); + Path metaDir = createPluginDir(temp); + Path pluginDir = metaDir.resolve("fake"); + Files.createDirectory(pluginDir); + writePlugin("fake", pluginDir, false); + Path configDir = pluginDir.resolve("config"); + Files.createDirectory(configDir); + Files.write(configDir.resolve("custom.yml"), "new config".getBytes(StandardCharsets.UTF_8)); + Files.createFile(configDir.resolve("other.yml")); + String pluginZip = createMetaPluginUrl("my_plugins", metaDir); + installPlugin(pluginZip, env.v1()); + assertMetaPlugin("my_plugins", "fake", pluginDir, env.v2()); + List configLines = Files.readAllLines(envConfigDir.resolve("custom.yml"), StandardCharsets.UTF_8); + assertEquals(1, configLines.size()); + assertEquals("existing config", configLines.get(0)); + assertTrue(Files.exists(envConfigDir.resolve("other.yml"))); + } + public void testConfigNotDir() throws Exception { Tuple env = createEnv(fs, temp); - Path pluginDir = createPluginDir(temp); + Path metaDir = createPluginDir(temp); + Path pluginDir = metaDir.resolve("fake"); + Files.createDirectories(pluginDir); Path configDir = pluginDir.resolve("config"); Files.createFile(configDir); String pluginZip = createPluginUrl("fake", pluginDir); UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip, env.v1())); assertTrue(e.getMessage(), e.getMessage().contains("not a directory")); assertInstallCleaned(env.v2()); + + String metaZip = createMetaPluginUrl("my_plugins", metaDir); + e = expectThrows(UserException.class, () -> installPlugin(metaZip, env.v1())); + assertTrue(e.getMessage(), e.getMessage().contains("not a directory")); + assertInstallCleaned(env.v2()); } public void testConfigContainsDir() throws Exception { @@ -619,26 +784,21 @@ public class InstallPluginCommandTests extends ESTestCase { assertInstallCleaned(env.v2()); } - public void testConfigConflict() throws Exception { - Tuple env = createEnv(fs, temp); - Path pluginDir = createPluginDir(temp); - Path configDir = pluginDir.resolve("config"); - Files.createDirectory(configDir); - Files.createFile(configDir.resolve("myconfig.yml")); - String pluginZip = createPluginUrl("elasticsearch.yml", pluginDir); - FileAlreadyExistsException e = expectThrows(FileAlreadyExistsException.class, () -> installPlugin(pluginZip, env.v1())); - assertTrue(e.getMessage(), e.getMessage().contains(env.v2().configFile().resolve("elasticsearch.yml").toString())); - assertInstallCleaned(env.v2()); - } - public void testMissingDescriptor() throws Exception { Tuple env = createEnv(fs, temp); - Path pluginDir = createPluginDir(temp); + Path metaDir = createPluginDir(temp); + Path pluginDir = metaDir.resolve("fake"); + Files.createDirectory(pluginDir); Files.createFile(pluginDir.resolve("fake.yml")); String pluginZip = writeZip(pluginDir, "elasticsearch").toUri().toURL().toString(); NoSuchFileException e = expectThrows(NoSuchFileException.class, () -> installPlugin(pluginZip, env.v1())); assertTrue(e.getMessage(), e.getMessage().contains("plugin-descriptor.properties")); assertInstallCleaned(env.v2()); + + String metaZip = createMetaPluginUrl("my_plugins", metaDir); + e = expectThrows(NoSuchFileException.class, () -> installPlugin(metaZip, env.v1())); + assertTrue(e.getMessage(), e.getMessage().contains("plugin-descriptor.properties")); + assertInstallCleaned(env.v2()); } public void testMissingDirectory() throws Exception { @@ -651,6 +811,16 @@ public class InstallPluginCommandTests extends ESTestCase { assertInstallCleaned(env.v2()); } + public void testMissingDirectoryMeta() throws Exception { + Tuple env = createEnv(fs, temp); + Path pluginDir = createPluginDir(temp); + Files.createFile(pluginDir.resolve(MetaPluginInfo.ES_META_PLUGIN_PROPERTIES)); + String pluginZip = writeZip(pluginDir, null).toUri().toURL().toString(); + UserException e = expectThrows(UserException.class, () -> installPlugin(pluginZip, env.v1())); + assertTrue(e.getMessage(), e.getMessage().contains("`elasticsearch` directory is missing in the plugin zip")); + assertInstallCleaned(env.v2()); + } + public void testZipRelativeOutsideEntryName() throws Exception { Tuple env = createEnv(fs, temp); Path zip = createTempDir().resolve("broken.zip"); @@ -748,6 +918,29 @@ public class InstallPluginCommandTests extends ESTestCase { "if you need to update the plugin, uninstall it first using command 'remove fake'")); } + public void testMetaPluginAlreadyInstalled() throws Exception { + Tuple env = createEnv(fs, temp); + { + // install fake plugin + Path pluginDir = createPluginDir(temp); + String pluginZip = createPluginUrl("fake", pluginDir); + installPlugin(pluginZip, env.v1()); + } + + Path pluginDir = createPluginDir(temp); + Files.createDirectory(pluginDir.resolve("fake")); + writePlugin("fake", pluginDir.resolve("fake"), false); + Files.createDirectory(pluginDir.resolve("other")); + writePlugin("other", pluginDir.resolve("other"), false); + String metaZip = createMetaPluginUrl("meta", pluginDir); + final UserException e = expectThrows(UserException.class, + () -> installPlugin(metaZip, env.v1(), randomFrom(skipJarHellCommand, defaultCommand))); + assertThat( + e.getMessage(), + equalTo("plugin directory [" + env.v2().pluginsFile().resolve("fake") + "] already exists; " + + "if you need to update the plugin, uninstall it first using command 'remove fake'")); + } + private void installPlugin(MockTerminal terminal, boolean isBatch) throws Exception { Tuple env = createEnv(fs, temp); Path pluginDir = createPluginDir(temp); @@ -791,7 +984,7 @@ public class InstallPluginCommandTests extends ESTestCase { return stagingHash; } @Override - void jarHellCheck(PluginInfo info, Path candidate, Path pluginsDir, Path modulesDir) throws Exception { + void jarHellCheck(PluginInfo candidateInfo, Path candidate, Path pluginsDir, Path modulesDir) throws Exception { // no jarhell check } }; @@ -951,6 +1144,17 @@ public class InstallPluginCommandTests extends ESTestCase { assertTrue(Files.exists(KeyStoreWrapper.keystorePath(env.v2().configFile()))); } + public void testKeystoreRequiredCreatedWithMetaPlugin() throws Exception { + Tuple env = createEnv(fs, temp); + Path metaDir = createPluginDir(temp); + Path pluginDir = metaDir.resolve("fake"); + Files.createDirectory(pluginDir); + writePlugin("fake", pluginDir, false, "requires.keystore", "true"); + String metaZip = createMetaPluginUrl("my_plugins", metaDir); + MockTerminal terminal = installPlugin(metaZip, env.v1()); + assertTrue(Files.exists(KeyStoreWrapper.keystorePath(env.v2().configFile()))); + } + private Function checksum(final MessageDigest digest) { return checksumAndString(digest, ""); } diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java index 4a243daf2ba..372a4cae8f2 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/ListPluginsCommandTests.java @@ -33,7 +33,6 @@ import org.apache.lucene.util.LuceneTestCase; import org.elasticsearch.Version; import org.elasticsearch.cli.ExitCodes; import org.elasticsearch.cli.MockTerminal; -import org.elasticsearch.cli.Terminal; import org.elasticsearch.cli.UserException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; @@ -94,18 +93,39 @@ public class ListPluginsCommandTests extends ESTestCase { final String description, final String name, final String classname) throws IOException { - buildFakePlugin(env, description, name, classname, false, false); + buildFakePlugin(env, null, description, name, classname, false, false); } private static void buildFakePlugin( final Environment env, + final String metaPlugin, + final String description, + final String name, + final String classname) throws IOException { + buildFakePlugin(env, metaPlugin, description, name, classname, false, false); + } + + private static void buildFakePlugin( + final Environment env, + final String description, + final String name, + final String classname, + final boolean hasNativeController, + final boolean requiresKeystore) throws IOException { + buildFakePlugin(env, null, description, name, classname, hasNativeController, requiresKeystore); + } + + private static void buildFakePlugin( + final Environment env, + final String metaPlugin, final String description, final String name, final String classname, final boolean hasNativeController, final boolean requiresKeystore) throws IOException { - PluginTestUtil.writeProperties( - env.pluginsFile().resolve(name), + Path dest = metaPlugin != null ? env.pluginsFile().resolve(metaPlugin) : env.pluginsFile(); + PluginTestUtil.writePluginProperties( + dest.resolve(name), "description", description, "name", name, "version", "1.0", @@ -116,6 +136,16 @@ public class ListPluginsCommandTests extends ESTestCase { "requires.keystore", Boolean.toString(requiresKeystore)); } + private static void buildFakeMetaPlugin( + final Environment env, + final String description, + final String name) throws IOException { + PluginTestUtil.writeMetaPluginProperties( + env.pluginsFile().resolve(name), + "description", description, + "name", name); + } + public void testPluginsDirMissing() throws Exception { Files.delete(env.pluginsFile()); IOException e = expectThrows(IOException.class, () -> listPlugins(home)); @@ -140,6 +170,16 @@ public class ListPluginsCommandTests extends ESTestCase { assertEquals(buildMultiline("fake1", "fake2"), terminal.getOutput()); } + public void testMetaPlugin() throws Exception { + buildFakeMetaPlugin(env, "fake meta desc", "meta_plugin"); + buildFakePlugin(env, "meta_plugin", "fake desc", "fake1", "org.fake1"); + buildFakePlugin(env, "meta_plugin", "fake desc 2", "fake2", "org.fake2"); + buildFakePlugin(env, "fake desc 3", "fake3", "org.fake3"); + buildFakePlugin(env, "fake desc 4", "fake4", "org.fake4"); + MockTerminal terminal = listPlugins(home); + assertEquals(buildMultiline("fake3", "fake4", "meta_plugin", "\tfake1", "\tfake2"), terminal.getOutput()); + } + public void testPluginWithVerbose() throws Exception { buildFakePlugin(env, "fake desc", "fake_plugin", "org.fake"); String[] params = { "-v" }; @@ -226,6 +266,37 @@ public class ListPluginsCommandTests extends ESTestCase { terminal.getOutput()); } + public void testPluginWithVerboseMetaPlugins() throws Exception { + buildFakeMetaPlugin(env, "fake meta desc", "meta_plugin"); + buildFakePlugin(env, "meta_plugin", "fake desc 1", "fake_plugin1", "org.fake"); + buildFakePlugin(env, "meta_plugin", "fake desc 2", "fake_plugin2", "org.fake2"); + String[] params = { "-v" }; + MockTerminal terminal = listPlugins(home, params); + assertEquals( + buildMultiline( + "Plugins directory: " + env.pluginsFile(), + "meta_plugin", + "\tfake_plugin1", + "\t- Plugin information:", + "\tName: fake_plugin1", + "\tDescription: fake desc 1", + "\tVersion: 1.0", + "\tNative Controller: false", + "\tRequires Keystore: false", + "\tExtended Plugins: []", + "\t * Classname: org.fake", + "\tfake_plugin2", + "\t- Plugin information:", + "\tName: fake_plugin2", + "\tDescription: fake desc 2", + "\tVersion: 1.0", + "\tNative Controller: false", + "\tRequires Keystore: false", + "\tExtended Plugins: []", + "\t * Classname: org.fake2"), + terminal.getOutput()); + } + public void testPluginWithoutVerboseMultiplePlugins() throws Exception { buildFakePlugin(env, "fake desc 1", "fake_plugin1", "org.fake"); buildFakePlugin(env, "fake desc 2", "fake_plugin2", "org.fake2"); @@ -243,7 +314,7 @@ public class ListPluginsCommandTests extends ESTestCase { public void testPluginWithWrongDescriptorFile() throws Exception{ final Path pluginDir = env.pluginsFile().resolve("fake1"); - PluginTestUtil.writeProperties(pluginDir, "description", "fake desc"); + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc"); IllegalArgumentException e = expectThrows( IllegalArgumentException.class, () -> listPlugins(home)); @@ -253,8 +324,21 @@ public class ListPluginsCommandTests extends ESTestCase { e.getMessage()); } + public void testMetaPluginWithWrongDescriptorFile() throws Exception{ + buildFakeMetaPlugin(env, "fake meta desc", "meta_plugin"); + final Path pluginDir = env.pluginsFile().resolve("meta_plugin").resolve("fake_plugin1"); + PluginTestUtil.writePluginProperties(pluginDir, "description", "fake desc"); + IllegalArgumentException e = expectThrows( + IllegalArgumentException.class, + () -> listPlugins(home)); + final Path descriptorPath = pluginDir.resolve(PluginInfo.ES_PLUGIN_PROPERTIES); + assertEquals( + "property [name] is missing in [" + descriptorPath.toString() + "]", + e.getMessage()); + } + public void testExistingIncompatiblePlugin() throws Exception { - PluginTestUtil.writeProperties(env.pluginsFile().resolve("fake_plugin1"), + PluginTestUtil.writePluginProperties(env.pluginsFile().resolve("fake_plugin1"), "description", "fake desc 1", "name", "fake_plugin1", "version", "1.0", @@ -278,4 +362,30 @@ public class ListPluginsCommandTests extends ESTestCase { assertEquals("fake_plugin1\nfake_plugin2\n", terminal.getOutput()); } + public void testExistingIncompatibleMetaPlugin() throws Exception { + buildFakeMetaPlugin(env, "fake meta desc", "meta_plugin"); + PluginTestUtil.writePluginProperties(env.pluginsFile().resolve("meta_plugin").resolve("fake_plugin1"), + "description", "fake desc 1", + "name", "fake_plugin1", + "version", "1.0", + "elasticsearch.version", Version.fromString("1.0.0").toString(), + "java.version", System.getProperty("java.specification.version"), + "classname", "org.fake1"); + buildFakePlugin(env, "fake desc 2", "fake_plugin2", "org.fake2"); + + MockTerminal terminal = listPlugins(home); + final String message = String.format(Locale.ROOT, + "plugin [%s] is incompatible with version [%s]; was designed for version [%s]", + "fake_plugin1", + Version.CURRENT.toString(), + "1.0.0"); + assertEquals( + "fake_plugin2\nmeta_plugin\n\tfake_plugin1\n" + "WARNING: " + message + "\n", + terminal.getOutput()); + + String[] params = {"-s"}; + terminal = listPlugins(home, params); + assertEquals("fake_plugin2\nmeta_plugin\n\tfake_plugin1\n", terminal.getOutput()); + } + } diff --git a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java index c128a245cd2..d15e0e642c8 100644 --- a/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java +++ b/distribution/tools/plugin-cli/src/test/java/org/elasticsearch/plugins/RemovePluginCommandTests.java @@ -79,8 +79,12 @@ public class RemovePluginCommandTests extends ESTestCase { } void createPlugin(String name) throws Exception { - PluginTestUtil.writeProperties( - env.pluginsFile().resolve(name), + createPlugin(env.pluginsFile(), name); + } + + void createPlugin(Path path, String name) throws Exception { + PluginTestUtil.writePluginProperties( + path.resolve(name), "description", "dummy", "name", name, "version", "1.0", @@ -89,6 +93,16 @@ public class RemovePluginCommandTests extends ESTestCase { "classname", "SomeClass"); } + void createMetaPlugin(String name, String... plugins) throws Exception { + PluginTestUtil.writeMetaPluginProperties( + env.pluginsFile().resolve(name), + "description", "dummy", + "name", name); + for (String plugin : plugins) { + createPlugin(env.pluginsFile().resolve(name), plugin); + } + } + static MockTerminal removePlugin(String name, Path home, boolean purge) throws Exception { Environment env = TestEnvironment.newEnvironment(Settings.builder().put("path.home", home).build()); MockTerminal terminal = new MockTerminal(); @@ -123,6 +137,19 @@ public class RemovePluginCommandTests extends ESTestCase { assertRemoveCleaned(env); } + public void testBasicMeta() throws Exception { + createMetaPlugin("meta", "fake1"); + createPlugin("other"); + removePlugin("meta", home, randomBoolean()); + assertFalse(Files.exists(env.pluginsFile().resolve("meta"))); + assertTrue(Files.exists(env.pluginsFile().resolve("other"))); + assertRemoveCleaned(env); + + UserException exc = + expectThrows(UserException.class, () -> removePlugin("fake1", home, randomBoolean())); + assertThat(exc.getMessage(), containsString("plugin [fake1] not found")); + } + public void testBin() throws Exception { createPlugin("fake"); Path binDir = env.binFile().resolve("fake"); diff --git a/docs/plugins/authors.asciidoc b/docs/plugins/authors.asciidoc index 8dc06d1433a..fd408e358fe 100644 --- a/docs/plugins/authors.asciidoc +++ b/docs/plugins/authors.asciidoc @@ -1,10 +1,18 @@ [[plugin-authors]] == Help for plugin authors +:plugin-properties-files: {docdir}/../../buildSrc/src/main/resources + The Elasticsearch repository contains examples of: * a https://github.com/elastic/elasticsearch/tree/master/plugins/jvm-example[Java plugin] which contains Java code. +* a https://github.com/elastic/elasticsearch/tree/master/plugins/examples/rescore[Java plugin] + which contains a rescore plugin. +* a https://github.com/elastic/elasticsearch/tree/master/plugins/examples/script-expert-scoring[Java plugin] + which contains a script plugin. +* a https://github.com/elastic/elasticsearch/tree/master/plugins/examples/meta-plugin[Java plugin] + which contains a meta plugin. These examples provide the bare bones needed to get started. For more information about how to write a plugin, we recommend looking at the plugins @@ -18,10 +26,13 @@ All plugin files must be contained in a directory called `elasticsearch`. [float] === Plugin descriptor file -All plugins must contain a file called `plugin-descriptor.properties` in the folder named `elasticsearch`. The format -for this file is described in detail here: +All plugins must contain a file called `plugin-descriptor.properties` in the folder named `elasticsearch`. +The format for this file is described in detail in this example: -https://github.com/elastic/elasticsearch/blob/master/buildSrc/src/main/resources/plugin-descriptor.properties[`/buildSrc/src/main/resources/plugin-descriptor.properties`]. +["source","properties",subs="attributes"] +-------------------------------------------------- +include-tagged::{plugin-properties-files}/plugin-descriptor.properties[plugin-descriptor.properties] +-------------------------------------------------- Either fill in this template yourself or, if you are using Elasticsearch's Gradle build system, you can fill in the necessary values in the `build.gradle` file for your plugin. @@ -112,3 +123,19 @@ AccessController.doPrivileged( See http://www.oracle.com/technetwork/java/seccodeguide-139067.html[Secure Coding Guidelines for Java SE] for more information. + +[float] +=== Meta Plugin + +It is also possible to bundle multiple plugins into a meta plugin. +A directory for each sub-plugin must be contained in a directory called `elasticsearch. +The meta plugin must also contain a file called `meta-plugin-descriptor.properties` in the directory named +`elasticsearch`. +The format for this file is described in detail in this example: + +["source","properties",subs="attributes"] +-------------------------------------------------- +include-tagged::{plugin-properties-files}/meta-plugin-descriptor.properties[meta-plugin-descriptor.properties] +-------------------------------------------------- + +A meta plugin can be installed/removed like a normal plugin with the `bin/elasticsearch-plugin` command. diff --git a/plugins/examples/meta-plugin/build.gradle b/plugins/examples/meta-plugin/build.gradle new file mode 100644 index 00000000000..3674837b0b2 --- /dev/null +++ b/plugins/examples/meta-plugin/build.gradle @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// A meta plugin packaging example that bundles multiple plugins in a single zip. +apply plugin: 'elasticsearch.standalone-rest-test' +apply plugin: 'elasticsearch.rest-test' + +File plugins = new File(buildDir, 'plugins-unzip') +subprojects { + // unzip the subproject plugins + task unzip(type:Copy, dependsOn: "${project.path}:bundlePlugin") { + File dest = new File(plugins, project.name) + from { zipTree(project(project.path).bundlePlugin.outputs.files.singleFile) } + eachFile { f -> f.path = f.path.replaceFirst('elasticsearch', '') } + into dest + } +} + +// Build the meta plugin zip from the subproject plugins (unzipped) +task buildZip(type:Zip) { + subprojects.each { dependsOn("${it.name}:unzip") } + from plugins + from 'src/main/resources/meta-plugin-descriptor.properties' + into 'elasticsearch' + includeEmptyDirs false +} + +integTestCluster { + dependsOn buildZip + + // This is important, so that all the modules are available too. + // There are index templates that use token filters that are in analysis-module and + // processors are being used that are in ingest-common module. + distribution = 'zip' + + // Install the meta plugin before start. + setupCommand 'installMetaPlugin', + 'bin/elasticsearch-plugin', 'install', 'file:' + buildZip.archivePath +} +check.dependsOn integTest diff --git a/plugins/examples/meta-plugin/dummy-plugin1/build.gradle b/plugins/examples/meta-plugin/dummy-plugin1/build.gradle new file mode 100644 index 00000000000..5a02e993f8c --- /dev/null +++ b/plugins/examples/meta-plugin/dummy-plugin1/build.gradle @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +apply plugin: 'elasticsearch.esplugin' + +esplugin { + name 'dummy-plugin1' + description 'A dummy plugin' + classname 'org.elasticsearch.example.DummyPlugin1' +} + +test.enabled = false +integTestRunner.enabled = false \ No newline at end of file diff --git a/plugins/examples/meta-plugin/dummy-plugin1/src/main/java/org/elasticsearch/example/DummyPlugin1.java b/plugins/examples/meta-plugin/dummy-plugin1/src/main/java/org/elasticsearch/example/DummyPlugin1.java new file mode 100644 index 00000000000..65102dbc2e3 --- /dev/null +++ b/plugins/examples/meta-plugin/dummy-plugin1/src/main/java/org/elasticsearch/example/DummyPlugin1.java @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.example; + +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.SearchPlugin; + +import java.util.List; + +import static java.util.Collections.singletonList; + +public class DummyPlugin1 extends Plugin {} diff --git a/plugins/examples/meta-plugin/dummy-plugin2/build.gradle b/plugins/examples/meta-plugin/dummy-plugin2/build.gradle new file mode 100644 index 00000000000..d90983adfed --- /dev/null +++ b/plugins/examples/meta-plugin/dummy-plugin2/build.gradle @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +apply plugin: 'elasticsearch.esplugin' + +esplugin { + name 'dummy-plugin2' + description 'Another dummy plugin' + classname 'org.elasticsearch.example.DummyPlugin2' +} + +test.enabled = false +integTestRunner.enabled = false \ No newline at end of file diff --git a/plugins/examples/meta-plugin/dummy-plugin2/src/main/java/org/elasticsearch/example/DummyPlugin2.java b/plugins/examples/meta-plugin/dummy-plugin2/src/main/java/org/elasticsearch/example/DummyPlugin2.java new file mode 100644 index 00000000000..2d74d7603d1 --- /dev/null +++ b/plugins/examples/meta-plugin/dummy-plugin2/src/main/java/org/elasticsearch/example/DummyPlugin2.java @@ -0,0 +1,29 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.example; + +import org.elasticsearch.plugins.Plugin; +import org.elasticsearch.plugins.SearchPlugin; + +import java.util.List; + +import static java.util.Collections.singletonList; + +public class DummyPlugin2 extends Plugin {} diff --git a/plugins/examples/meta-plugin/src/main/resources/meta-plugin-descriptor.properties b/plugins/examples/meta-plugin/src/main/resources/meta-plugin-descriptor.properties new file mode 100644 index 00000000000..1fd5a86b95a --- /dev/null +++ b/plugins/examples/meta-plugin/src/main/resources/meta-plugin-descriptor.properties @@ -0,0 +1,4 @@ +# The name of the meta plugin +name=my_meta_plugin +# The description of the meta plugin +description=A meta plugin example \ No newline at end of file diff --git a/plugins/examples/meta-plugin/src/test/java/org/elasticsearch/smoketest/SmokeTestPluginsClientYamlTestSuiteIT.java b/plugins/examples/meta-plugin/src/test/java/org/elasticsearch/smoketest/SmokeTestPluginsClientYamlTestSuiteIT.java new file mode 100644 index 00000000000..d1f9e6b7370 --- /dev/null +++ b/plugins/examples/meta-plugin/src/test/java/org/elasticsearch/smoketest/SmokeTestPluginsClientYamlTestSuiteIT.java @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.smoketest; + +import com.carrotsearch.randomizedtesting.annotations.Name; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; + +import org.elasticsearch.test.rest.yaml.ClientYamlTestCandidate; +import org.elasticsearch.test.rest.yaml.ESClientYamlSuiteTestCase; + +public class SmokeTestPluginsClientYamlTestSuiteIT extends ESClientYamlSuiteTestCase { + + public SmokeTestPluginsClientYamlTestSuiteIT(@Name("yaml") ClientYamlTestCandidate testCandidate) { + super(testCandidate); + } + + @ParametersFactory + public static Iterable parameters() throws Exception { + return ESClientYamlSuiteTestCase.createParameters(); + } +} + diff --git a/plugins/examples/meta-plugin/src/test/resources/rest-api-spec/test/smoke_test_plugins/10_basic.yml b/plugins/examples/meta-plugin/src/test/resources/rest-api-spec/test/smoke_test_plugins/10_basic.yml new file mode 100644 index 00000000000..011a278ed89 --- /dev/null +++ b/plugins/examples/meta-plugin/src/test/resources/rest-api-spec/test/smoke_test_plugins/10_basic.yml @@ -0,0 +1,14 @@ +# Integration tests for testing meta plugins +# +"Check meta plugin install": + - do: + cluster.state: {} + + # Get master node id + - set: { master_node: master } + + - do: + nodes.info: {} + + - match: { nodes.$master.plugins.0.name: dummy-plugin1 } + - match: { nodes.$master.plugins.1.name: dummy-plugin2 } diff --git a/qa/no-bootstrap-tests/src/test/java/org/elasticsearch/bootstrap/SpawnerNoBootstrapTests.java b/qa/no-bootstrap-tests/src/test/java/org/elasticsearch/bootstrap/SpawnerNoBootstrapTests.java index d9d4ab5c3ac..e4e603dff95 100644 --- a/qa/no-bootstrap-tests/src/test/java/org/elasticsearch/bootstrap/SpawnerNoBootstrapTests.java +++ b/qa/no-bootstrap-tests/src/test/java/org/elasticsearch/bootstrap/SpawnerNoBootstrapTests.java @@ -78,7 +78,7 @@ public class SpawnerNoBootstrapTests extends LuceneTestCase { // This plugin will NOT have a controller daemon Path plugin = environment.pluginsFile().resolve("a_plugin"); Files.createDirectories(plugin); - PluginTestUtil.writeProperties( + PluginTestUtil.writePluginProperties( plugin, "description", "a_plugin", "version", Version.CURRENT.toString(), @@ -114,7 +114,7 @@ public class SpawnerNoBootstrapTests extends LuceneTestCase { // this plugin will have a controller daemon Path plugin = environment.pluginsFile().resolve("test_plugin"); Files.createDirectories(plugin); - PluginTestUtil.writeProperties( + PluginTestUtil.writePluginProperties( plugin, "description", "test_plugin", "version", Version.CURRENT.toString(), @@ -129,7 +129,7 @@ public class SpawnerNoBootstrapTests extends LuceneTestCase { // this plugin will not have a controller daemon Path otherPlugin = environment.pluginsFile().resolve("other_plugin"); Files.createDirectories(otherPlugin); - PluginTestUtil.writeProperties( + PluginTestUtil.writePluginProperties( otherPlugin, "description", "other_plugin", "version", Version.CURRENT.toString(), @@ -163,6 +163,84 @@ public class SpawnerNoBootstrapTests extends LuceneTestCase { } } + /** + * Two plugins in a meta plugin - one with a controller daemon and one without. + */ + public void testControllerSpawnMetaPlugin() throws IOException, InterruptedException { + /* + * On Windows you can not directly run a batch file - you have to run cmd.exe with the batch + * file as an argument and that's out of the remit of the controller daemon process spawner. + */ + assumeFalse("This test does not work on Windows", Constants.WINDOWS); + + Path esHome = createTempDir().resolve("esHome"); + Settings.Builder settingsBuilder = Settings.builder(); + settingsBuilder.put(Environment.PATH_HOME_SETTING.getKey(), esHome.toString()); + Settings settings = settingsBuilder.build(); + + Environment environment = TestEnvironment.newEnvironment(settings); + + Path metaPlugin = environment.pluginsFile().resolve("meta_plugin"); + Files.createDirectories(metaPlugin); + PluginTestUtil.writeMetaPluginProperties( + metaPlugin, + "description", "test_plugin", + "name", "meta_plugin", + "plugins", "test_plugin,other_plugin"); + + // this plugin will have a controller daemon + Path plugin = metaPlugin.resolve("test_plugin"); + + Files.createDirectories(plugin); + PluginTestUtil.writePluginProperties( + plugin, + "description", "test_plugin", + "version", Version.CURRENT.toString(), + "elasticsearch.version", Version.CURRENT.toString(), + "name", "test_plugin", + "java.version", "1.8", + "classname", "TestPlugin", + "has.native.controller", "true"); + Path controllerProgram = Platforms.nativeControllerPath(plugin); + createControllerProgram(controllerProgram); + + // this plugin will not have a controller daemon + Path otherPlugin = metaPlugin.resolve("other_plugin"); + Files.createDirectories(otherPlugin); + PluginTestUtil.writePluginProperties( + otherPlugin, + "description", "other_plugin", + "version", Version.CURRENT.toString(), + "elasticsearch.version", Version.CURRENT.toString(), + "name", "other_plugin", + "java.version", "1.8", + "classname", "OtherPlugin", + "has.native.controller", "false"); + + Spawner spawner = new Spawner(); + spawner.spawnNativePluginControllers(environment); + + List processes = spawner.getProcesses(); + /* + * As there should only be a reference in the list for the plugin that had the controller + * daemon, we expect one here. + */ + assertThat(processes, hasSize(1)); + Process process = processes.get(0); + final InputStreamReader in = + new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8); + try (BufferedReader stdoutReader = new BufferedReader(in)) { + String line = stdoutReader.readLine(); + assertEquals("I am alive", line); + spawner.close(); + /* + * Fail if the process does not die within one second; usually it will be even quicker + * but it depends on OS scheduling. + */ + assertTrue(process.waitFor(1, TimeUnit.SECONDS)); + } + } + public void testControllerSpawnWithIncorrectDescriptor() throws IOException { // this plugin will have a controller daemon Path esHome = createTempDir().resolve("esHome"); @@ -174,7 +252,7 @@ public class SpawnerNoBootstrapTests extends LuceneTestCase { Path plugin = environment.pluginsFile().resolve("test_plugin"); Files.createDirectories(plugin); - PluginTestUtil.writeProperties( + PluginTestUtil.writePluginProperties( plugin, "description", "test_plugin", "version", Version.CURRENT.toString(), diff --git a/settings.gradle b/settings.gradle index cd6d2976e02..d4b280c346b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -90,15 +90,6 @@ List projects = [ 'qa:query-builder-bwc' ] -File examplePluginsDir = new File(rootProject.projectDir, 'plugins/examples') -List examplePlugins = [] -for (File example : examplePluginsDir.listFiles()) { - if (example.isDirectory() == false) continue; - if (example.name.startsWith('build') || example.name.startsWith('.')) continue; - projects.add("example-plugins:${example.name}".toString()) - examplePlugins.add(example.name) -} - projects.add("libs") File libsDir = new File(rootProject.projectDir, 'libs') for (File libDir : new File(rootProject.projectDir, 'libs').listFiles()) { @@ -124,11 +115,6 @@ if (isEclipse) { include projects.toArray(new String[0]) project(':build-tools').projectDir = new File(rootProject.projectDir, 'buildSrc') -project(':example-plugins').projectDir = new File(rootProject.projectDir, 'plugins/examples') - -for (String example : examplePlugins) { - project(":example-plugins:${example}").projectDir = new File(rootProject.projectDir, "plugins/examples/${example}") -} /* The BWC snapshot projects share the same build directory and build file, * but apply to different backwards compatibility branches. */ @@ -170,7 +156,7 @@ void addSubProjects(String path, File dir, List projects, List b } // TODO do we want to assert that there's nothing else in the bwc directory? } else { - if (path.isEmpty()) { + if (path.isEmpty() || path.startsWith(':example-plugins')) { project(projectName).projectDir = dir } for (File subdir : dir.listFiles()) { @@ -179,6 +165,15 @@ void addSubProjects(String path, File dir, List projects, List b } } +// include example plugins +File examplePluginsDir = new File(rootProject.projectDir, 'plugins/examples') +for (File example : examplePluginsDir.listFiles()) { + if (example.isDirectory() == false) continue; + if (example.name.startsWith('build') || example.name.startsWith('.')) continue; + addSubProjects(':example-plugins', example, projects, []) +} +project(':example-plugins').projectDir = new File(rootProject.projectDir, 'plugins/examples') + // look for extra plugins for elasticsearch File extraProjects = new File(rootProject.projectDir.parentFile, "${dirName}-extra") if (extraProjects.exists()) { diff --git a/test/framework/src/main/java/org/elasticsearch/plugins/PluginTestUtil.java b/test/framework/src/main/java/org/elasticsearch/plugins/PluginTestUtil.java index 10f4de2482a..5a92c99d618 100644 --- a/test/framework/src/main/java/org/elasticsearch/plugins/PluginTestUtil.java +++ b/test/framework/src/main/java/org/elasticsearch/plugins/PluginTestUtil.java @@ -27,12 +27,18 @@ import java.util.Properties; /** Utility methods for testing plugins */ public class PluginTestUtil { - + public static void writeMetaPluginProperties(Path pluginDir, String... stringProps) throws IOException { + writeProperties(pluginDir.resolve(MetaPluginInfo.ES_META_PLUGIN_PROPERTIES), stringProps); + } + + public static void writePluginProperties(Path pluginDir, String... stringProps) throws IOException { + writeProperties(pluginDir.resolve(PluginInfo.ES_PLUGIN_PROPERTIES), stringProps); + } + /** convenience method to write a plugin properties file */ - public static void writeProperties(Path pluginDir, String... stringProps) throws IOException { + private static void writeProperties(Path propertiesFile, String... stringProps) throws IOException { assert stringProps.length % 2 == 0; - Files.createDirectories(pluginDir); - Path propertiesFile = pluginDir.resolve(PluginInfo.ES_PLUGIN_PROPERTIES); + Files.createDirectories(propertiesFile.getParent()); Properties properties = new Properties(); for (int i = 0; i < stringProps.length; i += 2) { properties.put(stringProps[i], stringProps[i + 1]);