diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java index 5abe6a7d03..95d709fb4e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AbstractConfigurationService.java @@ -15,7 +15,7 @@ */ @SuppressWarnings("rawtypes") public class AbstractConfigurationService implements ConfigurationService { - private final Map configurations = new ConcurrentHashMap<>(); + private final Map configurations = new ConcurrentHashMap<>(); private final Version version; private KubernetesClient client; private Cloner cloner; @@ -88,12 +88,14 @@ private void put( ControllerConfiguration config, boolean failIfExisting) { final var name = config.getName(); if (failIfExisting) { - final var existing = configurations.get(name); + final var existing = getFor(name); if (existing != null) { throwExceptionOnNameCollision(config.getAssociatedReconcilerClassName(), existing); } } - configurations.put(name, config); + // record the configuration but mark is as un-configured in case a reconciler wants to override + // the configuration when registering + configurations.put(name, new Configured(false, config)); } protected void throwExceptionOnNameCollision( @@ -112,13 +114,32 @@ protected void throwExceptionOnNameCollision( public ControllerConfiguration getConfigurationFor( Reconciler reconciler) { final var key = keyFor(reconciler); - final var configuration = configurations.get(key); - if (configuration == null) { + var configured = configurations.get(key); + if (configured == null) { logMissingReconcilerWarning(key, getReconcilersNameMessage()); + return null; } - return configuration; + + var config = configured.config; + // if a reconciler is also a ConfigurableReconciler, update and replace its configuration if it + // hasn't already been configured + if (!configured.configured) { + if (reconciler instanceof ConfigurableReconciler configurableReconciler) { + + final var overrider = ControllerConfigurationOverrider.override(config); + configurableReconciler.updateConfigurationFrom(overrider); + config = overrider.build(); + } + // mark the reconciler as configured so that we don't attempt to do so again next time the + // configuration is requested + configurations.put(key, new Configured(true, config)); + } + + return config; } + private record Configured(boolean configured, ControllerConfiguration config) {} + protected void logMissingReconcilerWarning(String reconcilerKey, String reconcilersNameMessage) { log.warn("Cannot find reconciler named '{}'. {}", reconcilerKey, reconcilersNameMessage); } @@ -133,14 +154,14 @@ protected String keyFor(Reconciler reconciler) { return ReconcilerUtils.getNameFor(reconciler); } - @SuppressWarnings("unused") protected ControllerConfiguration getFor(String reconcilerName) { - return configurations.get(reconcilerName); + final var configured = configurations.get(reconcilerName); + return configured != null ? configured.config : null; } @SuppressWarnings("unused") protected Stream controllerConfigurations() { - return configurations.values().stream(); + return configurations.values().stream().map(Configured::config); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurableReconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurableReconciler.java new file mode 100644 index 0000000000..cd13b39bd7 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurableReconciler.java @@ -0,0 +1,25 @@ +package io.javaoperatorsdk.operator.api.config; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; + +/** + * Implement to change a {@link io.javaoperatorsdk.operator.api.reconciler.Reconciler}'s + * configuration at runtime + * + * @param

the primary resource type of the reconciler + * @since 5.1 + */ +public interface ConfigurableReconciler

{ + /** + * Updates the reconciler's configuration by applying the modifications specified by the provided + * {@link ControllerConfigurationOverrider} and then replacing the existing configuration in the + * {@link ConfigurationService} for this reconciler. Note that this method will not be applied if + * there is no configuration (as determined by {@link + * ConfigurationService#getConfigurationFor(Reconciler)} for the reconciler. + * + * @param configOverrider provides the modifications to apply to the existing reconciler's + * configuration + */ + void updateConfigurationFrom(ControllerConfigurationOverrider

configOverrider); +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java index 2f202436ff..4fbe037519 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/BaseConfigurationServiceTest.java @@ -17,6 +17,8 @@ import io.javaoperatorsdk.operator.ReconcilerUtils; import io.javaoperatorsdk.operator.api.config.AnnotationConfigurable; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurableReconciler; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import io.javaoperatorsdk.operator.api.config.dependent.ConfigurationConverter; import io.javaoperatorsdk.operator.api.config.dependent.Configured; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; @@ -311,6 +313,34 @@ void shouldUseSSAShouldAlsoWorkWithManualConfiguration() { configurationService.shouldUseSSA(reconciler.getSsaConfigMapDependent())); } + @Test + void shouldOverrideConfigurationForConfigurableReconciler() { + final var reconciler = new TestConfigurableReconciler(); + var config = configurationService.getConfigurationFor(reconciler); + assertThat(config.getInformerConfig().getLabelSelector()).isNull(); + + config = configurationService.getConfigurationFor(reconciler); + assertThat(config.getInformerConfig().getLabelSelector()) + .isEqualTo(TestConfigurableReconciler.selector); + } + + private static class TestConfigurableReconciler + implements Reconciler, ConfigurableReconciler { + private static final String selector = "foo=bar"; + + @Override + public UpdateControl reconcile(ConfigMap resource, Context context) + throws Exception { + return null; + } + + @Override + public void updateConfigurationFrom( + ControllerConfigurationOverrider configOverrider) { + configOverrider.withLabelSelector(selector); + } + } + @SuppressWarnings("unchecked") private static int getValue( io.javaoperatorsdk.operator.api.config.ControllerConfiguration configuration, int index) {