Skip to content

Commit 0a8fb4f

Browse files
committed
Add the ability to bundle multiple plugins into a meta plugin (#28022)
This commit adds the ability to package multiple plugins in a single zip. The zip file for a meta plugin must contains the following structure: |____elasticsearch/ | |____ <plugin1> <-- The plugin files for plugin1 (the content of the elastisearch directory) | |____ <plugin2> <-- The plugin files for plugin2 | |____ meta-plugin-descriptor.properties <-- example contents below The meta plugin properties descriptor is mandatory and must contain the following properties: description: simple summary of the meta plugin. name: the meta plugin name The installation process installs each plugin in a sub-folder inside the meta plugin directory. The example above would create the following structure in the plugins directory: |_____ plugins | |____ <name_of_the_meta_plugin> | | |____ meta-plugin-descriptor.properties | | |____ <plugin1> | | |____ <plugin2> If the sub plugins contain a config or a bin directory, they are copied in a sub folder inside the meta plugin config/bin directory. |_____ config | |____ <name_of_the_meta_plugin> | | |____ <plugin1> | | |____ <plugin2> |_____ bin | |____ <name_of_the_meta_plugin> | | |____ <plugin1> | | |____ <plugin2> The sub-plugins are loaded at startup like normal plugins with the same restrictions; they have a separate class loader and a sub-plugin cannot have the same name than another plugin (or a sub-plugin inside another meta plugin). It is also not possible to remove a sub-plugin inside a meta plugin, only full removal of the meta plugin is allowed. Closes #27316
1 parent 4600c21 commit 0a8fb4f

File tree

27 files changed

+1331
-215
lines changed

27 files changed

+1331
-215
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Elasticsearch meta plugin descriptor file
2+
# This file must exist as 'meta-plugin-descriptor.properties' in a folder named `elasticsearch`.
3+
#
4+
### example meta plugin for "meta-foo"
5+
#
6+
# meta-foo.zip <-- zip file for the meta plugin, with this structure:
7+
#|____elasticsearch/
8+
#| |____ <bundled_plugin_1> <-- The plugin files for bundled_plugin_1 (the content of the elastisearch directory)
9+
#| |____ <bundled_plugin_2> <-- The plugin files for bundled_plugin_2
10+
#| |____ meta-plugin-descriptor.properties <-- example contents below:
11+
#
12+
# description=My meta plugin
13+
# name=meta-foo
14+
#
15+
### mandatory elements for all meta plugins:
16+
#
17+
# 'description': simple summary of the meta plugin
18+
description=${description}
19+
#
20+
# 'name': the meta plugin name
21+
name=${name}

core/src/main/java/org/elasticsearch/action/admin/cluster/node/info/PluginsAndModules.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.io.IOException;
3131
import java.util.ArrayList;
3232
import java.util.Collections;
33+
import java.util.Comparator;
3334
import java.util.List;
3435

3536
/**
@@ -60,23 +61,23 @@ public void writeTo(StreamOutput out) throws IOException {
6061
*/
6162
public List<PluginInfo> getPluginInfos() {
6263
List<PluginInfo> plugins = new ArrayList<>(this.plugins);
63-
Collections.sort(plugins, (p1, p2) -> p1.getName().compareTo(p2.getName()));
64+
Collections.sort(plugins, Comparator.comparing(PluginInfo::getName));
6465
return plugins;
6566
}
66-
67+
6768
/**
6869
* Returns an ordered list based on modules name
6970
*/
7071
public List<PluginInfo> getModuleInfos() {
7172
List<PluginInfo> modules = new ArrayList<>(this.modules);
72-
Collections.sort(modules, (p1, p2) -> p1.getName().compareTo(p2.getName()));
73+
Collections.sort(modules, Comparator.comparing(PluginInfo::getName));
7374
return modules;
7475
}
7576

7677
public void addPlugin(PluginInfo info) {
7778
plugins.add(info);
7879
}
79-
80+
8081
public void addModule(PluginInfo info) {
8182
modules.add(info);
8283
}

core/src/main/java/org/elasticsearch/bootstrap/Security.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -163,16 +163,8 @@ static Map<String, URL> getCodebaseJarMap(Set<URL> urls) {
163163
static Map<String,Policy> getPluginPermissions(Environment environment) throws IOException, NoSuchAlgorithmException {
164164
Map<String,Policy> map = new HashMap<>();
165165
// collect up set of plugins and modules by listing directories.
166-
Set<Path> pluginsAndModules = new LinkedHashSet<>(); // order is already lost, but some filesystems have it
167-
if (Files.exists(environment.pluginsFile())) {
168-
try (DirectoryStream<Path> stream = Files.newDirectoryStream(environment.pluginsFile())) {
169-
for (Path plugin : stream) {
170-
if (pluginsAndModules.add(plugin) == false) {
171-
throw new IllegalStateException("duplicate plugin: " + plugin);
172-
}
173-
}
174-
}
175-
}
166+
Set<Path> pluginsAndModules = new LinkedHashSet<>(PluginInfo.extractAllPlugins(environment.pluginsFile()));
167+
176168
if (Files.exists(environment.modulesFile())) {
177169
try (DirectoryStream<Path> stream = Files.newDirectoryStream(environment.modulesFile())) {
178170
for (Path module : stream) {

core/src/main/java/org/elasticsearch/bootstrap/Spawner.java

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,12 @@
2121

2222
import org.apache.lucene.util.Constants;
2323
import org.apache.lucene.util.IOUtils;
24-
import org.elasticsearch.common.io.FileSystemUtils;
2524
import org.elasticsearch.env.Environment;
2625
import org.elasticsearch.plugins.Platforms;
2726
import org.elasticsearch.plugins.PluginInfo;
2827

2928
import java.io.Closeable;
3029
import java.io.IOException;
31-
import java.nio.file.DirectoryStream;
3230
import java.nio.file.Files;
3331
import java.nio.file.Path;
3432
import java.util.ArrayList;
@@ -72,27 +70,23 @@ void spawnNativePluginControllers(final Environment environment) throws IOExcept
7270
* For each plugin, attempt to spawn the controller daemon. Silently ignore any plugin that
7371
* don't include a controller for the correct platform.
7472
*/
75-
try (DirectoryStream<Path> stream = Files.newDirectoryStream(pluginsFile)) {
76-
for (final Path plugin : stream) {
77-
if (FileSystemUtils.isDesktopServicesStore(plugin)) {
78-
continue;
79-
}
80-
final PluginInfo info = PluginInfo.readFromProperties(plugin);
81-
final Path spawnPath = Platforms.nativeControllerPath(plugin);
82-
if (!Files.isRegularFile(spawnPath)) {
83-
continue;
84-
}
85-
if (!info.hasNativeController()) {
86-
final String message = String.format(
87-
Locale.ROOT,
88-
"plugin [%s] does not have permission to fork native controller",
89-
plugin.getFileName());
90-
throw new IllegalArgumentException(message);
91-
}
92-
final Process process =
93-
spawnNativePluginController(spawnPath, environment.tmpFile());
94-
processes.add(process);
73+
List<Path> paths = PluginInfo.extractAllPlugins(pluginsFile);
74+
for (Path plugin : paths) {
75+
final PluginInfo info = PluginInfo.readFromProperties(plugin);
76+
final Path spawnPath = Platforms.nativeControllerPath(plugin);
77+
if (!Files.isRegularFile(spawnPath)) {
78+
continue;
9579
}
80+
if (!info.hasNativeController()) {
81+
final String message = String.format(
82+
Locale.ROOT,
83+
"plugin [%s] does not have permission to fork native controller",
84+
plugin.getFileName());
85+
throw new IllegalArgumentException(message);
86+
}
87+
final Process process =
88+
spawnNativePluginController(spawnPath, environment.tmpFile());
89+
processes.add(process);
9690
}
9791
}
9892

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.plugins;
21+
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.util.Arrays;
27+
import java.util.Map;
28+
import java.util.Properties;
29+
import java.util.function.Function;
30+
import java.util.stream.Collectors;
31+
32+
/**
33+
* An in-memory representation of the meta plugin descriptor.
34+
*/
35+
public class MetaPluginInfo {
36+
static final String ES_META_PLUGIN_PROPERTIES = "meta-plugin-descriptor.properties";
37+
38+
private final String name;
39+
private final String description;
40+
41+
/**
42+
* Construct plugin info.
43+
*
44+
* @param name the name of the plugin
45+
* @param description a description of the plugin
46+
*/
47+
private MetaPluginInfo(String name, String description) {
48+
this.name = name;
49+
this.description = description;
50+
}
51+
52+
/**
53+
* @return Whether the provided {@code path} is a meta plugin.
54+
*/
55+
public static boolean isMetaPlugin(final Path path) {
56+
return Files.exists(path.resolve(ES_META_PLUGIN_PROPERTIES));
57+
}
58+
59+
/**
60+
* @return Whether the provided {@code path} is a meta properties file.
61+
*/
62+
public static boolean isPropertiesFile(final Path path) {
63+
return ES_META_PLUGIN_PROPERTIES.equals(path.getFileName().toString());
64+
}
65+
66+
/** reads (and validates) meta plugin metadata descriptor file */
67+
68+
/**
69+
* Reads and validates the meta plugin descriptor file.
70+
*
71+
* @param path the path to the root directory for the meta plugin
72+
* @return the meta plugin info
73+
* @throws IOException if an I/O exception occurred reading the meta plugin descriptor
74+
*/
75+
public static MetaPluginInfo readFromProperties(final Path path) throws IOException {
76+
final Path descriptor = path.resolve(ES_META_PLUGIN_PROPERTIES);
77+
78+
final Map<String, String> propsMap;
79+
{
80+
final Properties props = new Properties();
81+
try (InputStream stream = Files.newInputStream(descriptor)) {
82+
props.load(stream);
83+
}
84+
propsMap = props.stringPropertyNames().stream().collect(Collectors.toMap(Function.identity(), props::getProperty));
85+
}
86+
87+
final String name = propsMap.remove("name");
88+
if (name == null || name.isEmpty()) {
89+
throw new IllegalArgumentException(
90+
"property [name] is missing for meta plugin in [" + descriptor + "]");
91+
}
92+
final String description = propsMap.remove("description");
93+
if (description == null) {
94+
throw new IllegalArgumentException(
95+
"property [description] is missing for meta plugin [" + name + "]");
96+
}
97+
98+
if (propsMap.isEmpty() == false) {
99+
throw new IllegalArgumentException("Unknown properties in meta plugin descriptor: " + propsMap.keySet());
100+
}
101+
102+
return new MetaPluginInfo(name, description);
103+
}
104+
105+
/**
106+
* The name of the meta plugin.
107+
*
108+
* @return the meta plugin name
109+
*/
110+
public String getName() {
111+
return name;
112+
}
113+
114+
/**
115+
* The description of the meta plugin.
116+
*
117+
* @return the meta plugin description
118+
*/
119+
public String getDescription() {
120+
return description;
121+
}
122+
123+
@Override
124+
public boolean equals(Object o) {
125+
if (this == o) return true;
126+
if (o == null || getClass() != o.getClass()) return false;
127+
128+
MetaPluginInfo that = (MetaPluginInfo) o;
129+
130+
if (!name.equals(that.name)) return false;
131+
132+
return true;
133+
}
134+
135+
@Override
136+
public int hashCode() {
137+
return name.hashCode();
138+
}
139+
140+
@Override
141+
public String toString() {
142+
final StringBuilder information = new StringBuilder()
143+
.append("- Plugin information:\n")
144+
.append("Name: ").append(name).append("\n")
145+
.append("Description: ").append(description);
146+
return information.toString();
147+
}
148+
149+
}

core/src/main/java/org/elasticsearch/plugins/PluginInfo.java

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import org.elasticsearch.Version;
2323
import org.elasticsearch.bootstrap.JarHell;
2424
import org.elasticsearch.common.Booleans;
25+
import org.elasticsearch.common.Nullable;
2526
import org.elasticsearch.common.Strings;
27+
import org.elasticsearch.common.io.FileSystemUtils;
2628
import org.elasticsearch.common.io.stream.StreamInput;
2729
import org.elasticsearch.common.io.stream.StreamOutput;
2830
import org.elasticsearch.common.io.stream.Writeable;
@@ -31,14 +33,19 @@
3133

3234
import java.io.IOException;
3335
import java.io.InputStream;
36+
import java.nio.file.DirectoryStream;
3437
import java.nio.file.Files;
3538
import java.nio.file.Path;
39+
import java.util.ArrayList;
3640
import java.util.Arrays;
3741
import java.util.Collections;
42+
import java.util.HashSet;
43+
import java.util.LinkedList;
3844
import java.util.List;
3945
import java.util.Locale;
4046
import java.util.Map;
4147
import java.util.Properties;
48+
import java.util.Set;
4249
import java.util.function.Function;
4350
import java.util.stream.Collectors;
4451

@@ -125,7 +132,46 @@ public void writeTo(final StreamOutput out) throws IOException {
125132
}
126133
}
127134

128-
/** reads (and validates) plugin metadata descriptor file */
135+
/**
136+
* Extracts all {@link PluginInfo} from the provided {@code rootPath} expanding meta plugins if needed.
137+
* @param rootPath the path where the plugins are installed
138+
* @return A list of all plugin paths installed in the {@code rootPath}
139+
* @throws IOException if an I/O exception occurred reading the plugin descriptors
140+
*/
141+
public static List<Path> extractAllPlugins(final Path rootPath) throws IOException {
142+
final List<Path> plugins = new LinkedList<>(); // order is already lost, but some filesystems have it
143+
final Set<String> seen = new HashSet<>();
144+
if (Files.exists(rootPath)) {
145+
try (DirectoryStream<Path> stream = Files.newDirectoryStream(rootPath)) {
146+
for (Path plugin : stream) {
147+
if (FileSystemUtils.isDesktopServicesStore(plugin) ||
148+
plugin.getFileName().toString().startsWith(".removing-")) {
149+
continue;
150+
}
151+
if (seen.add(plugin.getFileName().toString()) == false) {
152+
throw new IllegalStateException("duplicate plugin: " + plugin);
153+
}
154+
if (MetaPluginInfo.isMetaPlugin(plugin)) {
155+
try (DirectoryStream<Path> subStream = Files.newDirectoryStream(plugin)) {
156+
for (Path subPlugin : subStream) {
157+
if (MetaPluginInfo.isPropertiesFile(subPlugin) ||
158+
FileSystemUtils.isDesktopServicesStore(subPlugin)) {
159+
continue;
160+
}
161+
if (seen.add(subPlugin.getFileName().toString()) == false) {
162+
throw new IllegalStateException("duplicate plugin: " + subPlugin);
163+
}
164+
plugins.add(subPlugin);
165+
}
166+
}
167+
} else {
168+
plugins.add(plugin);
169+
}
170+
}
171+
}
172+
}
173+
return plugins;
174+
}
129175

130176
/**
131177
* Reads and validates the plugin descriptor file.
@@ -341,16 +387,19 @@ public int hashCode() {
341387

342388
@Override
343389
public String toString() {
390+
return toString("");
391+
}
392+
393+
public String toString(String prefix) {
344394
final StringBuilder information = new StringBuilder()
345-
.append("- Plugin information:\n")
346-
.append("Name: ").append(name).append("\n")
347-
.append("Description: ").append(description).append("\n")
348-
.append("Version: ").append(version).append("\n")
349-
.append("Native Controller: ").append(hasNativeController).append("\n")
350-
.append("Requires Keystore: ").append(requiresKeystore).append("\n")
351-
.append("Extended Plugins: ").append(extendedPlugins).append("\n")
352-
.append(" * Classname: ").append(classname);
395+
.append(prefix).append("- Plugin information:\n")
396+
.append(prefix).append("Name: ").append(name).append("\n")
397+
.append(prefix).append("Description: ").append(description).append("\n")
398+
.append(prefix).append("Version: ").append(version).append("\n")
399+
.append(prefix).append("Native Controller: ").append(hasNativeController).append("\n")
400+
.append(prefix).append("Requires Keystore: ").append(requiresKeystore).append("\n")
401+
.append(prefix).append("Extended Plugins: ").append(extendedPlugins).append("\n")
402+
.append(prefix).append(" * Classname: ").append(classname);
353403
return information.toString();
354404
}
355-
356405
}

0 commit comments

Comments
 (0)