Introduce bootstrap configuration for plugin (#16205)

Bootstrap configuration can be part of a plugin
distribution but it will be loaded before the
actual plugin is instantiated. This allows to
e.g. specify a plugin-wide switch for a feature
which can then be evaluated early in the plugin
startup phase. If the feature is disabled, we
can skip loading configuration with potentially
required parameters and prevent validation
errors.

Co-authored-by: Laura Bergenthal-Grotlüschen <laura.bergenthalgrotlueschen@graylog.com>
This commit is contained in:
Othello Maurer
2023-09-05 16:16:37 +02:00
committed by GitHub
parent c738efb133
commit cd4e31052e
4 changed files with 82 additions and 42 deletions

View File

@@ -133,8 +133,9 @@ public abstract class CmdLineTool implements CliCommand {
protected String commandName = "command";
protected Injector injector;
protected Injector coreConfigInjector;
protected Injector bootstrapConfigInjector;
protected FeatureFlags featureFlags;
protected PluginLoader pluginLoader;
protected CmdLineTool(Configuration configuration) {
this(null, configuration);
@@ -158,7 +159,6 @@ public abstract class CmdLineTool implements CliCommand {
this.chainingClassLoader = new ChainingClassLoader(this.getClass().getClassLoader());
}
/**
* Validate the given configuration for this command.
*
@@ -268,21 +268,25 @@ public abstract class CmdLineTool implements CliCommand {
MetricRegistry metricRegistry = MetricRegistryFactory.create();
featureFlags = getFeatureFlags(metricRegistry);
pluginLoader = new PluginLoader(getPluginPath(configFile).toFile(), chainingClassLoader);
installCommandConfig();
installPluginBootstrapConfig(pluginLoader);
if (isDumpDefaultConfig()) {
dumpDefaultConfigAndExit();
}
installConfigRepositories();
installCommandConfig();
beforeStart();
beforeStart(parseAndGetTLSConfiguration(), parseAndGetPathConfiguration(configFile));
processConfiguration(jadConfig);
coreConfigInjector = setupCoreConfigInjector();
bootstrapConfigInjector = setupBootstrapConfigInjector();
final Set<Plugin> plugins = loadPlugins(getPluginPath(configFile), chainingClassLoader);
final Set<Plugin> plugins = loadPlugins();
installPluginConfig(plugins);
processConfiguration(jadConfig);
@@ -323,6 +327,10 @@ public abstract class CmdLineTool implements CliCommand {
startCommand();
}
private void installPluginBootstrapConfig(PluginLoader pluginLoader) {
pluginLoader.loadPluginBootstrapConfigs().forEach(jadConfig::addConfigurationBean);
}
// Parse only the TLSConfiguration bean
// to avoid triggering anything that might initialize the default SSLContext
private TLSProtocolsConfiguration parseAndGetTLSConfiguration() {
@@ -403,9 +411,8 @@ public abstract class CmdLineTool implements CliCommand {
}
private void dumpDefaultConfigAndExit() {
installCommandConfig();
coreConfigInjector = setupCoreConfigInjector();
installPluginConfig(loadPlugins(getPluginPath(configFile), chainingClassLoader));
bootstrapConfigInjector = setupBootstrapConfigInjector();
installPluginConfig(pluginLoader.loadPlugins(bootstrapConfigInjector));
dumpCurrentConfigAndExit();
}
@@ -420,15 +427,13 @@ public abstract class CmdLineTool implements CliCommand {
return new FeatureFlagsFactory().createImmutableFeatureFlags(customFeatureFlagFile, metricRegistry);
}
protected Set<Plugin> loadPlugins(Path pluginPath, ChainingClassLoader chainingClassLoader) {
protected Set<Plugin> loadPlugins() {
final Set<Plugin> plugins = new HashSet<>();
final PluginLoader pluginLoader = new PluginLoader(pluginPath.toFile(), chainingClassLoader,
coreConfigInjector);
for (Plugin plugin : pluginLoader.loadPlugins()) {
for (Plugin plugin : pluginLoader.loadPlugins(bootstrapConfigInjector)) {
final PluginMetaData metadata = plugin.metadata();
final Configuration config = coreConfigInjector.getInstance(Configuration.class);
final Configuration config = bootstrapConfigInjector.getInstance(Configuration.class);
// TODO do we want this here? We are also considering removing the deprecated CollectorPlugin entirely
if (config.isCloud()) {
if (metadata.getUniqueId().equals("org.graylog.plugins.collector.CollectorPlugin")) {
@@ -521,16 +526,16 @@ public abstract class CmdLineTool implements CliCommand {
}
/**
* Set up a separate injector, containing only the core configuration bindings. It can be used to look up
* configuration values in modules at binding time.
* Set up a separate injector, containing only the core and bootstrap configuration bindings.
* It can be used to look up configuration values in modules at binding time.
*/
protected Injector setupCoreConfigInjector() {
protected Injector setupBootstrapConfigInjector() {
final NamedConfigParametersModule configModule =
new NamedConfigParametersModule(jadConfig.getConfigurationBeans());
Injector coreConfigInjector = null;
Injector bootstrapConfigInjector = null;
try {
coreConfigInjector = Guice.createInjector(Stage.PRODUCTION, ImmutableList.of(configModule,
bootstrapConfigInjector = Guice.createInjector(Stage.PRODUCTION, ImmutableList.of(configModule,
(Module) Binder::requireExplicitBindings, this::featureFlagsBinding));
} catch (CreationException e) {
annotateInjectorCreationException(e);
@@ -538,12 +543,12 @@ public abstract class CmdLineTool implements CliCommand {
LOG.error("Injector creation failed!", e);
}
if (coreConfigInjector == null) {
LOG.error("Injector for core configuration could not be created, exiting! (Please include the previous " +
"error messages in bug reports.)");
if (bootstrapConfigInjector == null) {
LOG.error("Injector for bootstrap configuration could not be created, exiting! (Please include the " +
"previous error messages in bug reports.)");
System.exit(1);
}
return coreConfigInjector;
return bootstrapConfigInjector;
}
private void featureFlagsBinding(Binder binder) {

View File

@@ -31,9 +31,7 @@ import org.graylog2.shared.bindings.SchedulerBindings;
import org.graylog2.shared.bindings.ServerStatusBindings;
import org.graylog2.shared.journal.LocalKafkaJournal;
import org.graylog2.shared.journal.LocalKafkaJournalModule;
import org.graylog2.shared.plugins.ChainingClassLoader;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -79,7 +77,7 @@ public abstract class AbstractJournalCommand extends CmdLineTool {
}
@Override
protected Set<Plugin> loadPlugins(Path pluginPath, ChainingClassLoader chainingClassLoader) {
protected Set<Plugin> loadPlugins() {
// these commands do not need plugins, which could cause problems because of not loaded config beans
return Collections.emptySet();
}

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
package org.graylog2.plugin;
import org.graylog2.shared.plugins.PluginLoader;
/**
* A configuration bean to be processed by {@link com.github.joschi.jadconfig.JadConfig}.
* <p>
* Configurations implementing this interface should be loaded via the {@link java.util.ServiceLoader} mechanism. See
* {@link PluginLoader#loadPluginBootstrapConfigs()}.
*/
public interface PluginBootstrapConfig {
}

View File

@@ -24,6 +24,7 @@ import com.google.common.collect.Iterables;
import com.google.inject.Injector;
import org.apache.commons.io.FileUtils;
import org.graylog2.plugin.Plugin;
import org.graylog2.plugin.PluginBootstrapConfig;
import org.graylog2.plugin.PluginMetaData;
import org.graylog2.plugin.PluginModule;
import org.graylog2.plugin.PreflightCheckModule;
@@ -37,13 +38,11 @@ import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.util.Objects.requireNonNull;
@@ -54,18 +53,22 @@ public class PluginLoader {
private final File pluginDir;
private final ChainingClassLoader classLoader;
private final Injector coreConfigInjector;
public PluginLoader(File pluginDir, ChainingClassLoader classLoader, Injector coreConfigInjector) {
public PluginLoader(File pluginDir, ChainingClassLoader classLoader) {
this.pluginDir = requireNonNull(pluginDir);
this.classLoader = requireNonNull(classLoader);
this.coreConfigInjector = coreConfigInjector;
loadPluginJars();
}
public Set<Plugin> loadPlugins() {
public Set<PluginBootstrapConfig> loadPluginBootstrapConfigs() {
return ImmutableSet.copyOf(ServiceLoader.load(PluginBootstrapConfig.class, classLoader));
}
public Set<Plugin> loadPlugins(Injector injector) {
return ImmutableSortedSet.orderedBy(new PluginComparator())
.addAll(Iterables.transform(loadJarPlugins(), new PluginAdapterFunction()))
.addAll(Iterables.transform(loadClassPathPlugins(), new PluginAdapterFunction()))
.addAll(Iterables.transform(loadJarPlugins(), new PluginAdapterFunction(injector)))
.addAll(Iterables.transform(loadClassPathPlugins(), new PluginAdapterFunction(injector)))
.build();
}
@@ -73,16 +76,20 @@ public class PluginLoader {
return ServiceLoader.load(Plugin.class);
}
@SuppressForbidden("Deliberate invocation of URL#getFile()")
private Iterable<Plugin> loadJarPlugins() {
return ImmutableSet.copyOf(ServiceLoader.load(Plugin.class, classLoader));
}
@SuppressForbidden("Deliberate invocation of URL#getFile()")
private void loadPluginJars() {
if (!pluginDir.exists()) {
LOG.warn("Plugin directory {} does not exist, not loading plugins.", pluginDir.getAbsolutePath());
return Collections.emptySet();
return;
}
if (!pluginDir.isDirectory()) {
LOG.warn("Path {} is not a directory, cannot load plugins.", pluginDir);
return Collections.emptySet();
return;
}
LOG.debug("Scanning directory <{}> for plugins...", pluginDir.getAbsolutePath());
@@ -102,7 +109,7 @@ public class PluginLoader {
}
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
.toList();
final List<URL> sharedClassLoaderUrls = new ArrayList<>();
urls.forEach(url -> {
@@ -125,10 +132,6 @@ public class PluginLoader {
LOG.debug("Creating shared class loader for {} plugins: {}", sharedClassLoaderUrls.size(), sharedClassLoaderUrls);
classLoader.addClassLoader(URLClassLoader.newInstance(sharedClassLoaderUrls.toArray(new URL[0])));
}
final ServiceLoader<Plugin> pluginServiceLoader = ServiceLoader.load(Plugin.class, classLoader);
return ImmutableSet.copyOf(pluginServiceLoader);
}
public static class PluginComparator implements Comparator<Plugin> {
@@ -202,9 +205,15 @@ public class PluginLoader {
}
private class PluginAdapterFunction implements Function<Plugin, Plugin> {
private final Injector injector;
public PluginAdapterFunction(Injector injector) {
this.injector = injector;
}
@Override
public Plugin apply(Plugin input) {
coreConfigInjector.injectMembers(input);
injector.injectMembers(input);
return new PluginAdapter(input);
}
}