Skip to content

Commit 40dcbf3

Browse files
authored
Fix handling of mandatory meta plugins
This commit fixes an issue with setting plugin.mandatory to include a meta-plugin. The issue here is that the names that we collect are the underlying plugins, not the meta-plugin. We should not use the underlying plugins instead using the names of non-meta plugins and the names of meta-plugins. This commit addresses this. The strategy here is that when we look at the installed plugins on the filesystem, we keep track of which ones are meta-plugins and carry this information up to where check which plugins are installed against the mandatory plugins. Relates elastic#28710
1 parent 8149da4 commit 40dcbf3

File tree

2 files changed

+239
-33
lines changed

2 files changed

+239
-33
lines changed

server/src/main/java/org/elasticsearch/plugins/PluginsService.java

Lines changed: 120 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,16 @@
5656
import java.util.HashMap;
5757
import java.util.HashSet;
5858
import java.util.Iterator;
59+
import java.util.LinkedHashMap;
5960
import java.util.LinkedHashSet;
60-
import java.util.LinkedList;
6161
import java.util.List;
6262
import java.util.Locale;
6363
import java.util.Map;
6464
import java.util.Objects;
6565
import java.util.Set;
6666
import java.util.function.Function;
6767
import java.util.stream.Collectors;
68+
import java.util.stream.Stream;
6869

6970
import static org.elasticsearch.common.io.FileSystemUtils.isAccessibleDirectory;
7071

@@ -102,6 +103,8 @@ public PluginsService(Settings settings, Path configPath, Path modulesDirectory,
102103

103104
List<Tuple<PluginInfo, Plugin>> pluginsLoaded = new ArrayList<>();
104105
List<PluginInfo> pluginsList = new ArrayList<>();
106+
// we need to build a List of plugins for checking mandatory plugins
107+
final List<String> pluginsNames = new ArrayList<>();
105108
// first we load plugins that are on the classpath. this is for tests and transport clients
106109
for (Class<? extends Plugin> pluginClass : classpathPlugins) {
107110
Plugin plugin = loadPlugin(pluginClass, settings, configPath);
@@ -112,6 +115,7 @@ public PluginsService(Settings settings, Path configPath, Path modulesDirectory,
112115
}
113116
pluginsLoaded.add(new Tuple<>(pluginInfo, plugin));
114117
pluginsList.add(pluginInfo);
118+
pluginsNames.add(pluginInfo.getName());
115119
}
116120

117121
Set<Bundle> seenBundles = new LinkedHashSet<>();
@@ -135,11 +139,15 @@ public PluginsService(Settings settings, Path configPath, Path modulesDirectory,
135139
// TODO: remove this leniency, but tests bogusly rely on it
136140
if (isAccessibleDirectory(pluginsDirectory, logger)) {
137141
checkForFailedPluginRemovals(pluginsDirectory);
138-
Set<Bundle> plugins = getPluginBundles(pluginsDirectory);
139-
for (Bundle bundle : plugins) {
140-
pluginsList.add(bundle.plugin);
142+
List<BundleCollection> plugins = getPluginBundleCollections(pluginsDirectory);
143+
for (final BundleCollection plugin : plugins) {
144+
final Collection<Bundle> bundles = plugin.bundles();
145+
for (final Bundle bundle : bundles) {
146+
pluginsList.add(bundle.plugin);
147+
}
148+
seenBundles.addAll(bundles);
149+
pluginsNames.add(plugin.name());
141150
}
142-
seenBundles.addAll(plugins);
143151
}
144152
} catch (IOException ex) {
145153
throw new IllegalStateException("Unable to initialize plugins", ex);
@@ -152,12 +160,6 @@ public PluginsService(Settings settings, Path configPath, Path modulesDirectory,
152160
this.info = new PluginsAndModules(pluginsList, modulesList);
153161
this.plugins = Collections.unmodifiableList(pluginsLoaded);
154162

155-
// We need to build a List of plugins for checking mandatory plugins
156-
Set<String> pluginsNames = new HashSet<>();
157-
for (Tuple<PluginInfo, Plugin> tuple : this.plugins) {
158-
pluginsNames.add(tuple.v1().getName());
159-
}
160-
161163
// Checking expected plugins
162164
List<String> mandatoryPlugins = MANDATORY_SETTING.get(settings);
163165
if (mandatoryPlugins.isEmpty() == false) {
@@ -168,7 +170,11 @@ public PluginsService(Settings settings, Path configPath, Path modulesDirectory,
168170
}
169171
}
170172
if (!missingPlugins.isEmpty()) {
171-
throw new ElasticsearchException("Missing mandatory plugins [" + Strings.collectionToDelimitedString(missingPlugins, ", ") + "]");
173+
final String message = String.format(
174+
Locale.ROOT,
175+
"missing mandatory plugins [%s]",
176+
Strings.collectionToDelimitedString(missingPlugins, ", "));
177+
throw new IllegalStateException(message);
172178
}
173179
}
174180

@@ -244,9 +250,17 @@ public PluginsAndModules info() {
244250
return info;
245251
}
246252

253+
/**
254+
* An abstraction over a single plugin and meta-plugins.
255+
*/
256+
interface BundleCollection {
257+
String name();
258+
Collection<Bundle> bundles();
259+
}
260+
247261
// a "bundle" is a group of plugins in a single classloader
248262
// really should be 1-1, but we are not so fortunate
249-
static class Bundle {
263+
static class Bundle implements BundleCollection {
250264
final PluginInfo plugin;
251265
final Set<URL> urls;
252266

@@ -266,6 +280,16 @@ static class Bundle {
266280
this.urls = Objects.requireNonNull(urls);
267281
}
268282

283+
@Override
284+
public String name() {
285+
return plugin.getName();
286+
}
287+
288+
@Override
289+
public Collection<Bundle> bundles() {
290+
return Collections.singletonList(this);
291+
}
292+
269293
@Override
270294
public boolean equals(Object o) {
271295
if (this == o) return true;
@@ -281,35 +305,78 @@ public int hashCode() {
281305
}
282306

283307
/**
284-
* Extracts all installed plugin directories from the provided {@code rootPath} expanding meta plugins if needed.
308+
* Represents a meta-plugin and the {@link Bundle}s corresponding to its constituents.
309+
*/
310+
static class MetaBundle implements BundleCollection {
311+
private final String name;
312+
private final List<Bundle> bundles;
313+
314+
MetaBundle(final String name, final List<Bundle> bundles) {
315+
this.name = name;
316+
this.bundles = bundles;
317+
}
318+
319+
@Override
320+
public String name() {
321+
return name;
322+
}
323+
324+
@Override
325+
public Collection<Bundle> bundles() {
326+
return bundles;
327+
}
328+
329+
}
330+
331+
/**
332+
* Extracts all installed plugin directories from the provided {@code rootPath} expanding meta-plugins if needed.
333+
*
285334
* @param rootPath the path where the plugins are installed
286-
* @return A list of all plugin paths installed in the {@code rootPath}
335+
* @return a list of all plugin paths installed in the {@code rootPath}
287336
* @throws IOException if an I/O exception occurred reading the directories
288337
*/
289338
public static List<Path> findPluginDirs(final Path rootPath) throws IOException {
339+
final Tuple<List<Path>, Map<String, List<Path>>> groupedPluginDirs = findGroupedPluginDirs(rootPath);
340+
return Stream.concat(
341+
groupedPluginDirs.v1().stream(),
342+
groupedPluginDirs.v2().values().stream().flatMap(Collection::stream))
343+
.collect(Collectors.toList());
344+
}
345+
346+
/**
347+
* Extracts all installed plugin directories from the provided {@code rootPath} expanding meta-plugins if needed. The plugins are
348+
* grouped into plugins and meta-plugins. The meta-plugins are keyed by the meta-plugin name.
349+
*
350+
* @param rootPath the path where the plugins are installed
351+
* @return a tuple of plugins as the first component and meta-plugins keyed by meta-plugin name as the second component
352+
* @throws IOException if an I/O exception occurred reading the directories
353+
*/
354+
private static Tuple<List<Path>, Map<String, List<Path>>> findGroupedPluginDirs(final Path rootPath) throws IOException {
290355
final List<Path> plugins = new ArrayList<>();
356+
final Map<String, List<Path>> metaPlugins = new LinkedHashMap<>();
291357
final Set<String> seen = new HashSet<>();
292358
if (Files.exists(rootPath)) {
293359
try (DirectoryStream<Path> stream = Files.newDirectoryStream(rootPath)) {
294360
for (Path plugin : stream) {
295361
if (FileSystemUtils.isDesktopServicesStore(plugin) ||
296-
plugin.getFileName().toString().startsWith(".removing-")) {
362+
plugin.getFileName().toString().startsWith(".removing-")) {
297363
continue;
298364
}
299365
if (seen.add(plugin.getFileName().toString()) == false) {
300366
throw new IllegalStateException("duplicate plugin: " + plugin);
301367
}
302368
if (MetaPluginInfo.isMetaPlugin(plugin)) {
369+
final String name = plugin.getFileName().toString();
303370
try (DirectoryStream<Path> subStream = Files.newDirectoryStream(plugin)) {
304371
for (Path subPlugin : subStream) {
305372
if (MetaPluginInfo.isPropertiesFile(subPlugin) ||
306-
FileSystemUtils.isDesktopServicesStore(subPlugin)) {
373+
FileSystemUtils.isDesktopServicesStore(subPlugin)) {
307374
continue;
308375
}
309376
if (seen.add(subPlugin.getFileName().toString()) == false) {
310377
throw new IllegalStateException("duplicate plugin: " + subPlugin);
311378
}
312-
plugins.add(subPlugin);
379+
metaPlugins.computeIfAbsent(name, n -> new ArrayList<>()).add(subPlugin);
313380
}
314381
}
315382
} else {
@@ -318,7 +385,7 @@ public static List<Path> findPluginDirs(final Path rootPath) throws IOException
318385
}
319386
}
320387
}
321-
return plugins;
388+
return Tuple.tuple(plugins, metaPlugins);
322389
}
323390

324391
/**
@@ -380,26 +447,46 @@ static void checkForFailedPluginRemovals(final Path pluginsDirectory) throws IOE
380447
* @throws IOException if an I/O exception occurs reading the plugin bundles
381448
*/
382449
static Set<Bundle> getPluginBundles(final Path pluginsDirectory) throws IOException {
383-
Logger logger = Loggers.getLogger(PluginsService.class);
384-
Set<Bundle> bundles = new LinkedHashSet<>();
450+
return getPluginBundleCollections(pluginsDirectory).stream().flatMap(b -> b.bundles().stream()).collect(Collectors.toSet());
451+
}
385452

386-
List<Path> infos = findPluginDirs(pluginsDirectory);
387-
for (Path plugin : infos) {
388-
logger.trace("--- adding plugin [{}]", plugin.toAbsolutePath());
389-
final PluginInfo info;
390-
try {
391-
info = PluginInfo.readFromProperties(plugin);
392-
} catch (IOException e) {
393-
throw new IllegalStateException("Could not load plugin descriptor for existing plugin ["
394-
+ plugin.getFileName() + "]. Was the plugin built before 2.0?", e);
395-
}
396-
if (bundles.add(new Bundle(info, plugin)) == false) {
397-
throw new IllegalStateException("duplicate plugin: " + info);
453+
private static List<BundleCollection> getPluginBundleCollections(final Path pluginsDirectory) throws IOException {
454+
final List<BundleCollection> bundles = new ArrayList<>();
455+
final Set<Bundle> seenBundles = new HashSet<>();
456+
final Tuple<List<Path>, Map<String, List<Path>>> groupedPluginDirs = findGroupedPluginDirs(pluginsDirectory);
457+
for (final Path plugin : groupedPluginDirs.v1()) {
458+
final Bundle bundle = bundle(seenBundles, plugin);
459+
bundles.add(bundle);
460+
}
461+
for (final Map.Entry<String, List<Path>> metaPlugin : groupedPluginDirs.v2().entrySet()) {
462+
final List<Bundle> metaPluginBundles = new ArrayList<>();
463+
for (final Path metaPluginPlugin : metaPlugin.getValue()) {
464+
final Bundle bundle = bundle(seenBundles, metaPluginPlugin);
465+
metaPluginBundles.add(bundle);
398466
}
467+
final MetaBundle metaBundle = new MetaBundle(metaPlugin.getKey(), metaPluginBundles);
468+
bundles.add(metaBundle);
399469
}
470+
400471
return bundles;
401472
}
402473

474+
private static Bundle bundle(final Set<Bundle> bundles, final Path plugin) throws IOException {
475+
Loggers.getLogger(PluginsService.class).trace("--- adding plugin [{}]", plugin.toAbsolutePath());
476+
final PluginInfo info;
477+
try {
478+
info = PluginInfo.readFromProperties(plugin);
479+
} catch (final IOException e) {
480+
throw new IllegalStateException("Could not load plugin descriptor for existing plugin ["
481+
+ plugin.getFileName() + "]. Was the plugin built before 2.0?", e);
482+
}
483+
final Bundle bundle = new Bundle(info, plugin);
484+
if (bundles.add(bundle) == false) {
485+
throw new IllegalStateException("duplicate plugin: " + info);
486+
}
487+
return bundle;
488+
}
489+
403490
/**
404491
* Return the given bundles, sorted in dependency loading order.
405492
*

server/src/test/java/org/elasticsearch/plugins/PluginsServiceTests.java

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,4 +604,123 @@ public void testIncompatibleJavaVersion() throws Exception {
604604
IllegalStateException e = expectThrows(IllegalStateException.class, () -> PluginsService.verifyCompatibility(info));
605605
assertThat(e.getMessage(), containsString("my_plugin requires Java"));
606606
}
607+
608+
public void testFindPluginDirs() throws IOException {
609+
final Path plugins = createTempDir();
610+
611+
final Path fake = plugins.resolve("fake");
612+
613+
PluginTestUtil.writePluginProperties(
614+
fake,
615+
"description", "description",
616+
"name", "fake",
617+
"version", "1.0.0",
618+
"elasticsearch.version", Version.CURRENT.toString(),
619+
"java.version", System.getProperty("java.specification.version"),
620+
"classname", "test.DummyPlugin");
621+
622+
try (InputStream jar = PluginsServiceTests.class.getResourceAsStream("dummy-plugin.jar")) {
623+
Files.copy(jar, fake.resolve("plugin.jar"));
624+
}
625+
626+
final Path fakeMeta = plugins.resolve("fake-meta");
627+
628+
PluginTestUtil.writeMetaPluginProperties(fakeMeta, "description", "description", "name", "fake-meta");
629+
630+
final Path fakeMetaCore = fakeMeta.resolve("fake-meta-core");
631+
PluginTestUtil.writePluginProperties(
632+
fakeMetaCore,
633+
"description", "description",
634+
"name", "fake-meta-core",
635+
"version", "1.0.0",
636+
"elasticsearch.version", Version.CURRENT.toString(),
637+
"java.version", System.getProperty("java.specification.version"),
638+
"classname", "test.DummyPlugin");
639+
try (InputStream jar = PluginsServiceTests.class.getResourceAsStream("dummy-plugin.jar")) {
640+
Files.copy(jar, fakeMetaCore.resolve("plugin.jar"));
641+
}
642+
643+
assertThat(PluginsService.findPluginDirs(plugins), containsInAnyOrder(fake, fakeMetaCore));
644+
}
645+
646+
public void testMissingMandatoryPlugin() {
647+
final Settings settings =
648+
Settings.builder()
649+
.put("path.home", createTempDir())
650+
.put("plugin.mandatory", "fake")
651+
.build();
652+
final IllegalStateException e = expectThrows(IllegalStateException.class, () -> newPluginsService(settings));
653+
assertThat(e, hasToString(containsString("missing mandatory plugins [fake]")));
654+
}
655+
656+
public void testExistingMandatoryClasspathPlugin() {
657+
final Settings settings =
658+
Settings.builder()
659+
.put("path.home", createTempDir())
660+
.put("plugin.mandatory", "org.elasticsearch.plugins.PluginsServiceTests$FakePlugin")
661+
.build();
662+
newPluginsService(settings, FakePlugin.class);
663+
}
664+
665+
public static class FakePlugin extends Plugin {
666+
667+
public FakePlugin() {
668+
669+
}
670+
671+
}
672+
673+
public void testExistingMandatoryInstalledPlugin() throws IOException {
674+
final Path pathHome = createTempDir();
675+
final Path plugins = pathHome.resolve("plugins");
676+
final Path fake = plugins.resolve("fake");
677+
678+
PluginTestUtil.writePluginProperties(
679+
fake,
680+
"description", "description",
681+
"name", "fake",
682+
"version", "1.0.0",
683+
"elasticsearch.version", Version.CURRENT.toString(),
684+
"java.version", System.getProperty("java.specification.version"),
685+
"classname", "test.DummyPlugin");
686+
try (InputStream jar = PluginsServiceTests.class.getResourceAsStream("dummy-plugin.jar")) {
687+
Files.copy(jar, fake.resolve("plugin.jar"));
688+
}
689+
690+
final Settings settings =
691+
Settings.builder()
692+
.put("path.home", pathHome)
693+
.put("plugin.mandatory", "fake")
694+
.build();
695+
newPluginsService(settings);
696+
}
697+
698+
public void testExistingMandatoryMetaPlugin() throws IOException {
699+
final Path pathHome = createTempDir();
700+
final Path plugins = pathHome.resolve("plugins");
701+
final Path fakeMeta = plugins.resolve("fake-meta");
702+
703+
PluginTestUtil.writeMetaPluginProperties(fakeMeta, "description", "description", "name", "fake-meta");
704+
705+
final Path fake = fakeMeta.resolve("fake");
706+
PluginTestUtil.writePluginProperties(
707+
fake,
708+
"description", "description",
709+
"name", "fake",
710+
"version", "1.0.0",
711+
"elasticsearch.version", Version.CURRENT.toString(),
712+
"java.version", System.getProperty("java.specification.version"),
713+
"classname", "test.DummyPlugin");
714+
try (InputStream jar = PluginsServiceTests.class.getResourceAsStream("dummy-plugin.jar")) {
715+
Files.copy(jar, fake.resolve("plugin.jar"));
716+
}
717+
718+
final Settings settings =
719+
Settings.builder()
720+
.put("path.home", pathHome)
721+
.put("plugin.mandatory", "fake-meta")
722+
.build();
723+
newPluginsService(settings);
724+
}
725+
607726
}

0 commit comments

Comments
 (0)