From b819b31c9b0efdf2d4fe301597827ede46c60e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 22 Feb 2026 19:10:24 +0100 Subject: [PATCH 01/28] feat: configuration adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/loader/ConfigLoader.java | 19 +++++++++++++++++++ .../api/config/loader/ConfigProvider.java | 8 ++++++++ .../config/loader/DefatulConfigProvider.java | 3 +++ 3 files changed, 30 insertions(+) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java new file mode 100644 index 0000000000..122b701b80 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.api.config.loader; + +import java.util.function.Consumer; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; + +public class ConfigLoader { + + Consumer operatorConfigs() { + return null; + } + + Consumer> controllerConfigs( + String controllerName) { + return null; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java new file mode 100644 index 0000000000..e486c45311 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java @@ -0,0 +1,8 @@ +package io.javaoperatorsdk.operator.api.config.loader; + +import java.util.Optional; + +public interface ConfigProvider { + + Optional getConfig(String key, Class type); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java new file mode 100644 index 0000000000..fc97393e5e --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java @@ -0,0 +1,3 @@ +package io.javaoperatorsdk.operator.api.config.loader; + +public class DefatulConfigProvider implements ConfigProvider {} From 66bd1bfa8ff55c9bcbf14147c12a783be1622fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 22 Feb 2026 19:51:18 +0100 Subject: [PATCH 02/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/config/loader/ConfigLoader.java | 11 +++++++++++ .../operator/api/config/loader/ConfigProvider.java | 2 +- .../api/config/loader/DefatulConfigProvider.java | 10 +++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java index 122b701b80..151b6e8480 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java @@ -8,7 +8,18 @@ public class ConfigLoader { + private ConfigProvider configProvider; + + public ConfigLoader() { + this(new DefatulConfigProvider()); + } + + public ConfigLoader(ConfigProvider configProvider) { + this.configProvider = configProvider; + } + Consumer operatorConfigs() { + return null; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java index e486c45311..d9c6828651 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java @@ -4,5 +4,5 @@ public interface ConfigProvider { - Optional getConfig(String key, Class type); + Optional getValue(String key, Class type); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java index fc97393e5e..475b7c8c1b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java @@ -1,3 +1,11 @@ package io.javaoperatorsdk.operator.api.config.loader; -public class DefatulConfigProvider implements ConfigProvider {} +import java.util.Optional; + +public class DefatulConfigProvider implements ConfigProvider { + @Override + public Optional getValue(String key, Class type) { + + return Optional.empty(); + } +} From e608b6872d4a9fd9ca7b52aed912ec7f7d2c27da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 22 Feb 2026 19:52:12 +0100 Subject: [PATCH 03/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/api/config/loader/ConfigLoader.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java index 151b6e8480..cdebbf6452 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java @@ -18,13 +18,12 @@ public ConfigLoader(ConfigProvider configProvider) { this.configProvider = configProvider; } - Consumer operatorConfigs() { - + public Consumer applyConfigs() { return null; } - Consumer> controllerConfigs( - String controllerName) { + public + Consumer> applyControllerConfigs(String controllerName) { return null; } } From 8bda6e6466913e3e17e7073b89370ce686140842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sun, 22 Feb 2026 20:40:39 +0100 Subject: [PATCH 04/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/loader/ConfigBinding.java | 50 +++++ .../api/config/loader/ConfigLoader.java | 175 ++++++++++++++- .../api/config/loader/ConfigProvider.java | 25 +++ .../config/loader/DefatulConfigProvider.java | 11 - .../config/loader/DefaultConfigProvider.java | 62 ++++++ .../api/config/loader/ConfigBindingTest.java | 39 ++++ .../api/config/loader/ConfigLoaderTest.java | 207 ++++++++++++++++++ .../loader/DefaultConfigProviderTest.java | 98 +++++++++ 8 files changed, 652 insertions(+), 15 deletions(-) create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBinding.java delete mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java create mode 100644 operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBindingTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java create mode 100644 operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBinding.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBinding.java new file mode 100644 index 0000000000..069932b189 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBinding.java @@ -0,0 +1,50 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.api.config.loader; + +import java.util.function.BiConsumer; + +/** + * Associates a configuration key and its expected type with the setter that should be called on an + * overrider when the {@link ConfigProvider} returns a value for that key. + * + * @param the overrider type (e.g. {@code ConfigurationServiceOverrider}) + * @param the value type expected for this key + */ +public class ConfigBinding { + + private final String key; + private final Class type; + private final BiConsumer setter; + + public ConfigBinding(String key, Class type, BiConsumer setter) { + this.key = key; + this.type = type; + this.setter = setter; + } + + public String key() { + return key; + } + + public Class type() { + return type; + } + + public BiConsumer setter() { + return setter; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java index cdebbf6452..58722cde17 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java @@ -1,5 +1,22 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.api.config.loader; +import java.time.Duration; +import java.util.List; import java.util.function.Consumer; import io.fabric8.kubernetes.api.model.HasMetadata; @@ -8,22 +25,172 @@ public class ConfigLoader { - private ConfigProvider configProvider; + public static final ConfigLoader DEFAULT = new ConfigLoader(); + + /** + * Key prefix for operator-level (ConfigurationService) properties, e.g. {@code + * josdk.concurrent.reconciliation.threads}. + */ + public static final String OPERATOR_KEY_PREFIX = "josdk."; + + /** + * Key prefix for controller-level properties. The controller name is inserted between this prefix + * and the property name, e.g. {@code josdk.controller.my-controller.finalizer}. + */ + public static final String CONTROLLER_KEY_PREFIX = "josdk.controller."; + + // --------------------------------------------------------------------------- + // Operator-level (ConfigurationServiceOverrider) bindings + // Only scalar / value types that a key-value ConfigProvider can supply are + // included. Complex objects (KubernetesClient, ExecutorService, …) must be + // configured programmatically and are intentionally omitted. + // --------------------------------------------------------------------------- + private static final List> OPERATOR_BINDINGS = + List.of( + new ConfigBinding<>( + OPERATOR_KEY_PREFIX + "check.crd.and.validate.local.model", + Boolean.class, + ConfigurationServiceOverrider::checkingCRDAndValidateLocalModel), + new ConfigBinding<>( + OPERATOR_KEY_PREFIX + "concurrent.reconciliation.threads", + Integer.class, + ConfigurationServiceOverrider::withConcurrentReconciliationThreads), + new ConfigBinding<>( + OPERATOR_KEY_PREFIX + "concurrent.workflow.executor.threads", + Integer.class, + ConfigurationServiceOverrider::withConcurrentWorkflowExecutorThreads), + new ConfigBinding<>( + OPERATOR_KEY_PREFIX + "close.client.on.stop", + Boolean.class, + ConfigurationServiceOverrider::withCloseClientOnStop), + new ConfigBinding<>( + OPERATOR_KEY_PREFIX + "stop.on.informer.error.during.startup", + Boolean.class, + ConfigurationServiceOverrider::withStopOnInformerErrorDuringStartup), + new ConfigBinding<>( + OPERATOR_KEY_PREFIX + "cache.sync.timeout", + Duration.class, + ConfigurationServiceOverrider::withCacheSyncTimeout), + new ConfigBinding<>( + OPERATOR_KEY_PREFIX + "reconciliation.termination.timeout", + Duration.class, + ConfigurationServiceOverrider::withReconciliationTerminationTimeout), + new ConfigBinding<>( + OPERATOR_KEY_PREFIX + "ssa.based.create.update.match.for.dependent.resources", + Boolean.class, + ConfigurationServiceOverrider::withSSABasedCreateUpdateMatchForDependentResources), + new ConfigBinding<>( + OPERATOR_KEY_PREFIX + "use.ssa.to.patch.primary.resource", + Boolean.class, + ConfigurationServiceOverrider::withUseSSAToPatchPrimaryResource), + new ConfigBinding<>( + OPERATOR_KEY_PREFIX + "clone.secondary.resources.when.getting.from.cache", + Boolean.class, + ConfigurationServiceOverrider::withCloneSecondaryResourcesWhenGettingFromCache)); + + // --------------------------------------------------------------------------- + // Controller-level (ControllerConfigurationOverrider) bindings + // The key used at runtime is built as: + // CONTROLLER_KEY_PREFIX + controllerName + "." + + // --------------------------------------------------------------------------- + private static final List, ?>> + CONTROLLER_BINDINGS = + List.of( + new ConfigBinding<>( + "finalizer", String.class, ControllerConfigurationOverrider::withFinalizer), + new ConfigBinding<>( + "generation.aware", + Boolean.class, + ControllerConfigurationOverrider::withGenerationAware), + new ConfigBinding<>( + "label.selector", + String.class, + ControllerConfigurationOverrider::withLabelSelector), + new ConfigBinding<>( + "reconciliation.max.interval", + Duration.class, + ControllerConfigurationOverrider::withReconciliationMaxInterval), + new ConfigBinding<>( + "field.manager", + String.class, + ControllerConfigurationOverrider::withFieldManager), + new ConfigBinding<>( + "trigger.reconciler.on.all.events", + Boolean.class, + ControllerConfigurationOverrider::withTriggerReconcilerOnAllEvents), + new ConfigBinding<>( + "informer.list.limit", + Long.class, + ControllerConfigurationOverrider::withInformerListLimit)); + + private final ConfigProvider configProvider; public ConfigLoader() { - this(new DefatulConfigProvider()); + this(new DefaultConfigProvider()); } public ConfigLoader(ConfigProvider configProvider) { this.configProvider = configProvider; } + /** + * Returns a {@link Consumer} that applies every operator-level property found in the {@link + * ConfigProvider} to the given {@link ConfigurationServiceOverrider}. Returns {@code null} when + * no binding has a matching value, preserving the previous behaviour. + */ public Consumer applyConfigs() { - return null; + return buildConsumer(OPERATOR_BINDINGS, null); } + /** + * Returns a {@link Consumer} that applies every controller-level property found in the {@link + * ConfigProvider} to the given {@link ControllerConfigurationOverrider}. The keys are looked up + * as {@code josdk.controller..}. Returns {@code null} when no binding + * has a matching value. + */ + @SuppressWarnings("unchecked") public Consumer> applyControllerConfigs(String controllerName) { - return null; + String prefix = CONTROLLER_KEY_PREFIX + controllerName + "."; + // Cast is safe: the setter BiConsumer, T> is covariant in + // its first parameter for our usage – we only ever call it with + // ControllerConfigurationOverrider. + List, ?>> bindings = + (List, ?>>) (List) CONTROLLER_BINDINGS; + return buildConsumer(bindings, prefix); + } + + /** + * Iterates {@code bindings} and, for each one whose key (optionally prefixed by {@code + * keyPrefix}) is present in the {@link ConfigProvider}, accumulates a call to the binding's + * setter. + * + * @param bindings the predefined bindings to check + * @param keyPrefix when non-null the key stored in the binding is treated as a suffix and this + * prefix is prepended before the lookup + * @return a consumer that applies all found values, or {@code null} if none were found + */ + private Consumer buildConsumer(List> bindings, String keyPrefix) { + Consumer consumer = null; + for (var binding : bindings) { + String lookupKey = keyPrefix == null ? binding.key() : keyPrefix + binding.key(); + Consumer step = resolveStep(binding, lookupKey); + if (step != null) { + consumer = consumer == null ? step : consumer.andThen(step); + } + } + return consumer; + } + + /** + * Queries the {@link ConfigProvider} for {@code key} with the binding's type. If a value is + * present, returns a {@link Consumer} that calls the binding's setter; otherwise returns {@code + * null}. + */ + private Consumer resolveStep(ConfigBinding binding, String key) { + return configProvider + .getValue(key, binding.type()) + .map(value -> (Consumer) overrider -> binding.setter().accept(overrider, value)) + .orElse(null); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java index d9c6828651..9279439d68 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java @@ -1,8 +1,33 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.api.config.loader; import java.util.Optional; public interface ConfigProvider { + /** + * Returns the value associated with {@code key}, converted to {@code type}, or an empty {@link + * Optional} if the key is not set. + * + * @param key the dot-separated configuration key, e.g. {@code josdk.cache.sync.timeout} + * @param type the expected type of the value; supported types depend on the implementation + * @param the value type + * @return an {@link Optional} containing the typed value, or empty if the key is absent + * @throws IllegalArgumentException if {@code type} is not supported by the implementation + */ Optional getValue(String key, Class type); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java deleted file mode 100644 index 475b7c8c1b..0000000000 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefatulConfigProvider.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.javaoperatorsdk.operator.api.config.loader; - -import java.util.Optional; - -public class DefatulConfigProvider implements ConfigProvider { - @Override - public Optional getValue(String key, Class type) { - - return Optional.empty(); - } -} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java new file mode 100644 index 0000000000..df0f41792c --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java @@ -0,0 +1,62 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.api.config.loader; + +import java.time.Duration; +import java.util.Optional; + +public class DefaultConfigProvider implements ConfigProvider { + + /** + * Looks up {@code key} first as an environment variable (dots and hyphens replaced by + * underscores, uppercased, e.g. {@code josdk.cache.sync.timeout} → {@code + * JOSDK_CACHE_SYNC_TIMEOUT}), then as a system property with the key as-is. The environment + * variable takes precedence when both are set. + */ + @Override + @SuppressWarnings("unchecked") + public Optional getValue(String key, Class type) { + String raw = resolveRaw(key); + if (raw == null) { + return Optional.empty(); + } + return Optional.of(type.cast(convert(raw, type))); + } + + private String resolveRaw(String key) { + String envKey = key.replace('.', '_').replace('-', '_').toUpperCase(); + String envValue = System.getenv(envKey); + if (envValue != null) { + return envValue; + } + return System.getProperty(key); + } + + private Object convert(String raw, Class type) { + if (type == String.class) { + return raw; + } else if (type == Boolean.class) { + return Boolean.parseBoolean(raw); + } else if (type == Integer.class) { + return Integer.parseInt(raw); + } else if (type == Long.class) { + return Long.parseLong(raw); + } else if (type == Duration.class) { + return Duration.parse(raw); + } + throw new IllegalArgumentException("Unsupported config type: " + type.getName()); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBindingTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBindingTest.java new file mode 100644 index 0000000000..6a1c7aeecd --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBindingTest.java @@ -0,0 +1,39 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.api.config.loader; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ConfigBindingTest { + + @Test + void storesKeyTypeAndSetter() { + List calls = new ArrayList<>(); + ConfigBinding, String> binding = + new ConfigBinding<>("my.key", String.class, (list, v) -> list.add(v)); + + assertThat(binding.key()).isEqualTo("my.key"); + assertThat(binding.type()).isEqualTo(String.class); + + binding.setter().accept(calls, "hello"); + assertThat(calls).containsExactly("hello"); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java new file mode 100644 index 0000000000..6635f11962 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java @@ -0,0 +1,207 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.api.config.loader; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationService; + +import static org.assertj.core.api.Assertions.assertThat; + +class ConfigLoaderTest { + + // A simple ConfigProvider backed by a plain map for test control. + private static ConfigProvider mapProvider(Map values) { + return new ConfigProvider() { + @Override + @SuppressWarnings("unchecked") + public Optional getValue(String key, Class type) { + return Optional.ofNullable((T) values.get(key)); + } + }; + } + + // -- applyConfigs ----------------------------------------------------------- + + @Test + void applyConfigsReturnsNullWhenNothingConfigured() { + var loader = new ConfigLoader(mapProvider(Map.of())); + assertThat(loader.applyConfigs()).isNull(); + } + + @Test + void applyConfigsAppliesConcurrentReconciliationThreads() { + var loader = + new ConfigLoader(mapProvider(Map.of("josdk.concurrent.reconciliation.threads", 7))); + + var base = new BaseConfigurationService(null); + var result = + ConfigurationService.newOverriddenConfigurationService(base, loader.applyConfigs()); + + assertThat(result.concurrentReconciliationThreads()).isEqualTo(7); + } + + @Test + void applyConfigsAppliesConcurrentWorkflowExecutorThreads() { + var loader = + new ConfigLoader(mapProvider(Map.of("josdk.concurrent.workflow.executor.threads", 3))); + + var base = new BaseConfigurationService(null); + var result = + ConfigurationService.newOverriddenConfigurationService(base, loader.applyConfigs()); + + assertThat(result.concurrentWorkflowExecutorThreads()).isEqualTo(3); + } + + @Test + void applyConfigsAppliesBooleanFlags() { + var values = new HashMap(); + values.put("josdk.check.crd.and.validate.local.model", true); + values.put("josdk.close.client.on.stop", false); + values.put("josdk.stop.on.informer.error.during.startup", false); + values.put("josdk.ssa.based.create.update.match.for.dependent.resources", false); + values.put("josdk.use.ssa.to.patch.primary.resource", false); + values.put("josdk.clone.secondary.resources.when.getting.from.cache", true); + var loader = new ConfigLoader(mapProvider(values)); + + var base = new BaseConfigurationService(null); + var result = + ConfigurationService.newOverriddenConfigurationService(base, loader.applyConfigs()); + + assertThat(result.checkCRDAndValidateLocalModel()).isTrue(); + assertThat(result.closeClientOnStop()).isFalse(); + assertThat(result.stopOnInformerErrorDuringStartup()).isFalse(); + assertThat(result.ssaBasedCreateUpdateMatchForDependentResources()).isFalse(); + assertThat(result.useSSAToPatchPrimaryResource()).isFalse(); + assertThat(result.cloneSecondaryResourcesWhenGettingFromCache()).isTrue(); + } + + @Test + void applyConfigsAppliesDurations() { + var values = new HashMap(); + values.put("josdk.cache.sync.timeout", Duration.ofSeconds(10)); + values.put("josdk.reconciliation.termination.timeout", Duration.ofSeconds(5)); + var loader = new ConfigLoader(mapProvider(values)); + + var base = new BaseConfigurationService(null); + var result = + ConfigurationService.newOverriddenConfigurationService(base, loader.applyConfigs()); + + assertThat(result.cacheSyncTimeout()).isEqualTo(Duration.ofSeconds(10)); + assertThat(result.reconciliationTerminationTimeout()).isEqualTo(Duration.ofSeconds(5)); + } + + @Test + void applyConfigsOnlyAppliesPresentKeys() { + // Only one key present — other defaults must be unchanged. + var loader = + new ConfigLoader(mapProvider(Map.of("josdk.concurrent.reconciliation.threads", 12))); + + var base = new BaseConfigurationService(null); + var result = + ConfigurationService.newOverriddenConfigurationService(base, loader.applyConfigs()); + + assertThat(result.concurrentReconciliationThreads()).isEqualTo(12); + // Default unchanged + assertThat(result.concurrentWorkflowExecutorThreads()) + .isEqualTo(base.concurrentWorkflowExecutorThreads()); + } + + // -- applyControllerConfigs ------------------------------------------------- + + @Test + void applyControllerConfigsReturnsNullWhenNothingConfigured() { + var loader = new ConfigLoader(mapProvider(Map.of())); + assertThat(loader.applyControllerConfigs("my-controller")).isNull(); + } + + @Test + void applyControllerConfigsQueriesKeysPrefixedWithControllerName() { + // Record every key the loader asks for, regardless of whether a value exists. + var queriedKeys = new ArrayList(); + ConfigProvider recordingProvider = + new ConfigProvider() { + @Override + public Optional getValue(String key, Class type) { + queriedKeys.add(key); + return Optional.empty(); + } + }; + + new ConfigLoader(recordingProvider).applyControllerConfigs("my-ctrl"); + + assertThat(queriedKeys).allMatch(k -> k.startsWith("josdk.controller.my-ctrl.")); + } + + @Test + void applyControllerConfigsIsolatesControllersByName() { + // Two controllers configured in the same provider — only matching keys must be returned. + var values = new HashMap(); + values.put("josdk.controller.alpha.finalizer", "alpha-finalizer"); + values.put("josdk.controller.beta.finalizer", "beta-finalizer"); + var loader = new ConfigLoader(mapProvider(values)); + + // alpha gets a consumer (key found), beta gets a consumer (key found) + assertThat(loader.applyControllerConfigs("alpha")).isNotNull(); + assertThat(loader.applyControllerConfigs("beta")).isNotNull(); + // a controller with no configured keys gets null + assertThat(loader.applyControllerConfigs("gamma")).isNull(); + } + + @Test + void applyControllerConfigsQueriesAllExpectedPropertySuffixes() { + var queriedKeys = new ArrayList(); + ConfigProvider recordingProvider = + new ConfigProvider() { + @Override + public Optional getValue(String key, Class type) { + queriedKeys.add(key); + return Optional.empty(); + } + }; + + new ConfigLoader(recordingProvider).applyControllerConfigs("ctrl"); + + assertThat(queriedKeys) + .contains( + "josdk.controller.ctrl.finalizer", + "josdk.controller.ctrl.generation.aware", + "josdk.controller.ctrl.label.selector", + "josdk.controller.ctrl.reconciliation.max.interval", + "josdk.controller.ctrl.field.manager", + "josdk.controller.ctrl.trigger.reconciler.on.all.events", + "josdk.controller.ctrl.informer.list.limit"); + } + + // -- key prefix constants --------------------------------------------------- + + @Test + void operatorKeyPrefixIsJosdkDot() { + assertThat(ConfigLoader.OPERATOR_KEY_PREFIX).isEqualTo("josdk."); + } + + @Test + void controllerKeyPrefixIsJosdkControllerDot() { + assertThat(ConfigLoader.CONTROLLER_KEY_PREFIX).isEqualTo("josdk.controller."); + } +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java new file mode 100644 index 0000000000..7ef413699e --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java @@ -0,0 +1,98 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.api.config.loader; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class DefaultConfigProviderTest { + + private final DefaultConfigProvider provider = new DefaultConfigProvider(); + + // -- system property tests -------------------------------------------------- + + @Test + void returnsEmptyWhenNeitherEnvNorPropertyIsSet() { + assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty(); + } + + @Test + void readsStringFromSystemProperty() { + System.setProperty("josdk.test.string", "hello"); + try { + assertThat(provider.getValue("josdk.test.string", String.class)).hasValue("hello"); + } finally { + System.clearProperty("josdk.test.string"); + } + } + + @Test + void readsBooleanFromSystemProperty() { + System.setProperty("josdk.test.bool", "true"); + try { + assertThat(provider.getValue("josdk.test.bool", Boolean.class)).hasValue(true); + } finally { + System.clearProperty("josdk.test.bool"); + } + } + + @Test + void readsIntegerFromSystemProperty() { + System.setProperty("josdk.test.integer", "42"); + try { + assertThat(provider.getValue("josdk.test.integer", Integer.class)).hasValue(42); + } finally { + System.clearProperty("josdk.test.integer"); + } + } + + @Test + void readsLongFromSystemProperty() { + System.setProperty("josdk.test.long", "123456789"); + try { + assertThat(provider.getValue("josdk.test.long", Long.class)).hasValue(123456789L); + } finally { + System.clearProperty("josdk.test.long"); + } + } + + @Test + void readsDurationFromSystemProperty() { + System.setProperty("josdk.test.duration", "PT30S"); + try { + assertThat(provider.getValue("josdk.test.duration", Duration.class)) + .hasValue(Duration.ofSeconds(30)); + } finally { + System.clearProperty("josdk.test.duration"); + } + } + + @Test + void throwsForUnsupportedType() { + System.setProperty("josdk.test.unsupported", "value"); + try { + assertThatIllegalArgumentException() + .isThrownBy(() -> provider.getValue("josdk.test.unsupported", Double.class)) + .withMessageContaining("Unsupported config type"); + } finally { + System.clearProperty("josdk.test.unsupported"); + } + } +} From 2cae73eda85505b0711a90d17c2c6b03d41dea42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Feb 2026 15:40:24 +0100 Subject: [PATCH 05/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/loader/ConfigLoader.java | 64 ++++++++++--------- .../config/loader/DefaultConfigProvider.java | 13 +++- .../api/config/loader/ConfigLoaderTest.java | 39 ++++++----- .../loader/DefaultConfigProviderTest.java | 40 +++++++++++- 4 files changed, 104 insertions(+), 52 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java index 58722cde17..6c7811c863 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java @@ -27,17 +27,16 @@ public class ConfigLoader { public static final ConfigLoader DEFAULT = new ConfigLoader(); - /** - * Key prefix for operator-level (ConfigurationService) properties, e.g. {@code - * josdk.concurrent.reconciliation.threads}. - */ - public static final String OPERATOR_KEY_PREFIX = "josdk."; + public static final String DEFAULT_OPERATOR_KEY_PREFIX = "josdk."; + public static final String DEFAULT_CONTROLLER_KEY_PREFIX = "josdk.controller."; /** * Key prefix for controller-level properties. The controller name is inserted between this prefix * and the property name, e.g. {@code josdk.controller.my-controller.finalizer}. */ - public static final String CONTROLLER_KEY_PREFIX = "josdk.controller."; + private final String controllerKeyPrefix; + + private final String operatorKeyPrefix; // --------------------------------------------------------------------------- // Operator-level (ConfigurationServiceOverrider) bindings @@ -48,43 +47,43 @@ public class ConfigLoader { private static final List> OPERATOR_BINDINGS = List.of( new ConfigBinding<>( - OPERATOR_KEY_PREFIX + "check.crd.and.validate.local.model", + "check-crd", Boolean.class, ConfigurationServiceOverrider::checkingCRDAndValidateLocalModel), new ConfigBinding<>( - OPERATOR_KEY_PREFIX + "concurrent.reconciliation.threads", + "reconciliation.termination-timeout", + Duration.class, + ConfigurationServiceOverrider::withReconciliationTerminationTimeout), + new ConfigBinding<>( + "reconciliation.concurrent-threads", Integer.class, ConfigurationServiceOverrider::withConcurrentReconciliationThreads), new ConfigBinding<>( - OPERATOR_KEY_PREFIX + "concurrent.workflow.executor.threads", + "workflow.executor-threads", Integer.class, ConfigurationServiceOverrider::withConcurrentWorkflowExecutorThreads), new ConfigBinding<>( - OPERATOR_KEY_PREFIX + "close.client.on.stop", + "close-client-on-stop", Boolean.class, ConfigurationServiceOverrider::withCloseClientOnStop), new ConfigBinding<>( - OPERATOR_KEY_PREFIX + "stop.on.informer.error.during.startup", + "informer.stop-on-error-during-startup", Boolean.class, ConfigurationServiceOverrider::withStopOnInformerErrorDuringStartup), new ConfigBinding<>( - OPERATOR_KEY_PREFIX + "cache.sync.timeout", + "informer.cache-sync-timeout", Duration.class, ConfigurationServiceOverrider::withCacheSyncTimeout), new ConfigBinding<>( - OPERATOR_KEY_PREFIX + "reconciliation.termination.timeout", - Duration.class, - ConfigurationServiceOverrider::withReconciliationTerminationTimeout), - new ConfigBinding<>( - OPERATOR_KEY_PREFIX + "ssa.based.create.update.match.for.dependent.resources", + "dependent-resources.ssa-based-create-update-match", Boolean.class, ConfigurationServiceOverrider::withSSABasedCreateUpdateMatchForDependentResources), new ConfigBinding<>( - OPERATOR_KEY_PREFIX + "use.ssa.to.patch.primary.resource", + "use-ssa-to-patch-primary-resource", Boolean.class, ConfigurationServiceOverrider::withUseSSAToPatchPrimaryResource), new ConfigBinding<>( - OPERATOR_KEY_PREFIX + "clone.secondary.resources.when.getting.from.cache", + "clone-secondary-resources-when-getting-from-cache", Boolean.class, ConfigurationServiceOverrider::withCloneSecondaryResourcesWhenGettingFromCache)); @@ -99,47 +98,54 @@ public class ConfigLoader { new ConfigBinding<>( "finalizer", String.class, ControllerConfigurationOverrider::withFinalizer), new ConfigBinding<>( - "generation.aware", + "generation-aware", Boolean.class, ControllerConfigurationOverrider::withGenerationAware), new ConfigBinding<>( - "label.selector", + "label-selector", String.class, ControllerConfigurationOverrider::withLabelSelector), new ConfigBinding<>( - "reconciliation.max.interval", + "max-reconciliation-interval", Duration.class, ControllerConfigurationOverrider::withReconciliationMaxInterval), new ConfigBinding<>( - "field.manager", + "field-manager", String.class, ControllerConfigurationOverrider::withFieldManager), new ConfigBinding<>( - "trigger.reconciler.on.all.events", + "trigger-reconciler-on-all-events", Boolean.class, ControllerConfigurationOverrider::withTriggerReconcilerOnAllEvents), new ConfigBinding<>( - "informer.list.limit", + "informer-list-limit", Long.class, ControllerConfigurationOverrider::withInformerListLimit)); private final ConfigProvider configProvider; public ConfigLoader() { - this(new DefaultConfigProvider()); + this(new DefaultConfigProvider(), DEFAULT_CONTROLLER_KEY_PREFIX, DEFAULT_OPERATOR_KEY_PREFIX); } public ConfigLoader(ConfigProvider configProvider) { + this(configProvider, DEFAULT_CONTROLLER_KEY_PREFIX, DEFAULT_OPERATOR_KEY_PREFIX); + } + + public ConfigLoader( + ConfigProvider configProvider, String controllerKeyPrefix, String operatorKeyPrefix) { this.configProvider = configProvider; + this.controllerKeyPrefix = controllerKeyPrefix; + this.operatorKeyPrefix = operatorKeyPrefix; } /** * Returns a {@link Consumer} that applies every operator-level property found in the {@link * ConfigProvider} to the given {@link ConfigurationServiceOverrider}. Returns {@code null} when - * no binding has a matching value, preserving the previous behaviour. + * no binding has a matching value, preserving the previous behavior. */ public Consumer applyConfigs() { - return buildConsumer(OPERATOR_BINDINGS, null); + return buildConsumer(OPERATOR_BINDINGS, operatorKeyPrefix); } /** @@ -151,7 +157,7 @@ public Consumer applyConfigs() { @SuppressWarnings("unchecked") public Consumer> applyControllerConfigs(String controllerName) { - String prefix = CONTROLLER_KEY_PREFIX + controllerName + "."; + String prefix = controllerKeyPrefix + controllerName + "."; // Cast is safe: the setter BiConsumer, T> is covariant in // its first parameter for our usage – we only ever call it with // ControllerConfigurationOverrider. diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java index df0f41792c..a73ba5b6a5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java @@ -17,9 +17,20 @@ import java.time.Duration; import java.util.Optional; +import java.util.function.Function; public class DefaultConfigProvider implements ConfigProvider { + private final Function envLookup; + + public DefaultConfigProvider() { + this(System::getenv); + } + + DefaultConfigProvider(Function envLookup) { + this.envLookup = envLookup; + } + /** * Looks up {@code key} first as an environment variable (dots and hyphens replaced by * underscores, uppercased, e.g. {@code josdk.cache.sync.timeout} → {@code @@ -38,7 +49,7 @@ public Optional getValue(String key, Class type) { private String resolveRaw(String key) { String envKey = key.replace('.', '_').replace('-', '_').toUpperCase(); - String envValue = System.getenv(envKey); + String envValue = envLookup.apply(envKey); if (envValue != null) { return envValue; } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java index 6635f11962..c31c27cafd 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java @@ -52,7 +52,7 @@ void applyConfigsReturnsNullWhenNothingConfigured() { @Test void applyConfigsAppliesConcurrentReconciliationThreads() { var loader = - new ConfigLoader(mapProvider(Map.of("josdk.concurrent.reconciliation.threads", 7))); + new ConfigLoader(mapProvider(Map.of("josdk.reconciliation.concurrent-threads", 7))); var base = new BaseConfigurationService(null); var result = @@ -63,8 +63,7 @@ void applyConfigsAppliesConcurrentReconciliationThreads() { @Test void applyConfigsAppliesConcurrentWorkflowExecutorThreads() { - var loader = - new ConfigLoader(mapProvider(Map.of("josdk.concurrent.workflow.executor.threads", 3))); + var loader = new ConfigLoader(mapProvider(Map.of("josdk.workflow.executor-threads", 3))); var base = new BaseConfigurationService(null); var result = @@ -76,12 +75,12 @@ void applyConfigsAppliesConcurrentWorkflowExecutorThreads() { @Test void applyConfigsAppliesBooleanFlags() { var values = new HashMap(); - values.put("josdk.check.crd.and.validate.local.model", true); - values.put("josdk.close.client.on.stop", false); - values.put("josdk.stop.on.informer.error.during.startup", false); - values.put("josdk.ssa.based.create.update.match.for.dependent.resources", false); - values.put("josdk.use.ssa.to.patch.primary.resource", false); - values.put("josdk.clone.secondary.resources.when.getting.from.cache", true); + values.put("josdk.check-crd", true); + values.put("josdk.close-client-on-stop", false); + values.put("josdk.informer.stop-on-error-during-startup", false); + values.put("josdk.dependent-resources.ssa-based-create-update-match", false); + values.put("josdk.use-ssa-to-patch-primary-resource", false); + values.put("josdk.clone-secondary-resources-when-getting-from-cache", true); var loader = new ConfigLoader(mapProvider(values)); var base = new BaseConfigurationService(null); @@ -99,8 +98,8 @@ void applyConfigsAppliesBooleanFlags() { @Test void applyConfigsAppliesDurations() { var values = new HashMap(); - values.put("josdk.cache.sync.timeout", Duration.ofSeconds(10)); - values.put("josdk.reconciliation.termination.timeout", Duration.ofSeconds(5)); + values.put("josdk.informer.cache-sync-timeout", Duration.ofSeconds(10)); + values.put("josdk.reconciliation.termination-timeout", Duration.ofSeconds(5)); var loader = new ConfigLoader(mapProvider(values)); var base = new BaseConfigurationService(null); @@ -115,7 +114,7 @@ void applyConfigsAppliesDurations() { void applyConfigsOnlyAppliesPresentKeys() { // Only one key present — other defaults must be unchanged. var loader = - new ConfigLoader(mapProvider(Map.of("josdk.concurrent.reconciliation.threads", 12))); + new ConfigLoader(mapProvider(Map.of("josdk.reconciliation.concurrent-threads", 12))); var base = new BaseConfigurationService(null); var result = @@ -185,23 +184,23 @@ public Optional getValue(String key, Class type) { assertThat(queriedKeys) .contains( "josdk.controller.ctrl.finalizer", - "josdk.controller.ctrl.generation.aware", - "josdk.controller.ctrl.label.selector", - "josdk.controller.ctrl.reconciliation.max.interval", - "josdk.controller.ctrl.field.manager", - "josdk.controller.ctrl.trigger.reconciler.on.all.events", - "josdk.controller.ctrl.informer.list.limit"); + "josdk.controller.ctrl.generation-aware", + "josdk.controller.ctrl.label-selector", + "josdk.controller.ctrl.max-reconciliation-interval", + "josdk.controller.ctrl.field-manager", + "josdk.controller.ctrl.trigger-reconciler-on-all-events", + "josdk.controller.ctrl.informer-list-limit"); } // -- key prefix constants --------------------------------------------------- @Test void operatorKeyPrefixIsJosdkDot() { - assertThat(ConfigLoader.OPERATOR_KEY_PREFIX).isEqualTo("josdk."); + assertThat(ConfigLoader.DEFAULT_OPERATOR_KEY_PREFIX).isEqualTo("josdk."); } @Test void controllerKeyPrefixIsJosdkControllerDot() { - assertThat(ConfigLoader.CONTROLLER_KEY_PREFIX).isEqualTo("josdk.controller."); + assertThat(ConfigLoader.DEFAULT_CONTROLLER_KEY_PREFIX).isEqualTo("josdk.controller."); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java index 7ef413699e..a042c7dff4 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java @@ -26,13 +26,49 @@ class DefaultConfigProviderTest { private final DefaultConfigProvider provider = new DefaultConfigProvider(); - // -- system property tests -------------------------------------------------- - @Test void returnsEmptyWhenNeitherEnvNorPropertyIsSet() { assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty(); } + // -- env variable tests ----------------------------------------------------- + + @Test + void readsStringFromEnvVariable() { + var envProvider = new DefaultConfigProvider(k -> k.equals("JOSDK_TEST_STRING") ? "from-env" : null); + assertThat(envProvider.getValue("josdk.test.string", String.class)).hasValue("from-env"); + } + + @Test + void envVariableKeyUsesUppercaseWithUnderscores() { + // dots and hyphens both become underscores, key is uppercased + var envProvider = new DefaultConfigProvider(k -> k.equals("JOSDK_CACHE_SYNC_TIMEOUT") ? "PT10S" : null); + assertThat(envProvider.getValue("josdk.cache-sync.timeout", Duration.class)) + .hasValue(Duration.ofSeconds(10)); + } + + @Test + void envVariableTakesPrecedenceOverSystemProperty() { + System.setProperty("josdk.test.precedence", "from-sysprop"); + try { + var envProvider = new DefaultConfigProvider(k -> k.equals("JOSDK_TEST_PRECEDENCE") ? "from-env" : null); + assertThat(envProvider.getValue("josdk.test.precedence", String.class)).hasValue("from-env"); + } finally { + System.clearProperty("josdk.test.precedence"); + } + } + + @Test + void fallsBackToSystemPropertyWhenEnvVariableAbsent() { + System.setProperty("josdk.test.fallback", "from-sysprop"); + try { + var envProvider = new DefaultConfigProvider(k -> null); + assertThat(envProvider.getValue("josdk.test.fallback", String.class)).hasValue("from-sysprop"); + } finally { + System.clearProperty("josdk.test.fallback"); + } + } + @Test void readsStringFromSystemProperty() { System.setProperty("josdk.test.string", "hello"); From 3ce65a419db3b9e27d71ba2b05d43ca0b6e6708e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Feb 2026 15:57:08 +0100 Subject: [PATCH 06/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/loader/ConfigLoader.java | 52 ++++++++++++++++++- .../config/loader/DefaultConfigProvider.java | 2 + .../loader/DefaultConfigProviderTest.java | 15 ++++-- 3 files changed, 63 insertions(+), 6 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java index 6c7811c863..679c096b99 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java @@ -17,11 +17,13 @@ import java.time.Duration; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; +import io.javaoperatorsdk.operator.processing.retry.GenericRetry; public class ConfigLoader { @@ -87,6 +89,14 @@ public class ConfigLoader { Boolean.class, ConfigurationServiceOverrider::withCloneSecondaryResourcesWhenGettingFromCache)); + // --------------------------------------------------------------------------- + // Controller-level retry property suffixes + // --------------------------------------------------------------------------- + static final String RETRY_MAX_ATTEMPTS_SUFFIX = "retry.max-attempts"; + static final String RETRY_INITIAL_INTERVAL_SUFFIX = "retry.initial-interval"; + static final String RETRY_INTERVAL_MULTIPLIER_SUFFIX = "retry.interval-multiplier"; + static final String RETRY_MAX_INTERVAL_SUFFIX = "retry.max-interval"; + // --------------------------------------------------------------------------- // Controller-level (ControllerConfigurationOverrider) bindings // The key used at runtime is built as: @@ -163,7 +173,47 @@ Consumer> applyControllerConfigs(String cont // ControllerConfigurationOverrider. List, ?>> bindings = (List, ?>>) (List) CONTROLLER_BINDINGS; - return buildConsumer(bindings, prefix); + Consumer> consumer = buildConsumer(bindings, prefix); + + Consumer> retryStep = buildRetryConsumer(prefix); + if (retryStep != null) { + consumer = consumer == null ? retryStep : consumer.andThen(retryStep); + } + + return consumer; + } + + /** + * If at least one retry property is present for the given prefix, returns a {@link Consumer} that + * builds a {@link GenericRetry} starting from {@link GenericRetry#defaultLimitedExponentialRetry} + * and overrides only the properties that are explicitly set. + */ + private Consumer> buildRetryConsumer( + String prefix) { + Optional maxAttempts = + configProvider.getValue(prefix + RETRY_MAX_ATTEMPTS_SUFFIX, Integer.class); + Optional initialInterval = + configProvider.getValue(prefix + RETRY_INITIAL_INTERVAL_SUFFIX, Long.class); + Optional intervalMultiplier = + configProvider.getValue(prefix + RETRY_INTERVAL_MULTIPLIER_SUFFIX, Double.class); + Optional maxInterval = + configProvider.getValue(prefix + RETRY_MAX_INTERVAL_SUFFIX, Long.class); + + if (maxAttempts.isEmpty() + && initialInterval.isEmpty() + && intervalMultiplier.isEmpty() + && maxInterval.isEmpty()) { + return null; + } + + return overrider -> { + GenericRetry retry = GenericRetry.defaultLimitedExponentialRetry(); + maxAttempts.ifPresent(retry::setMaxAttempts); + initialInterval.ifPresent(retry::setInitialInterval); + intervalMultiplier.ifPresent(retry::setIntervalMultiplier); + maxInterval.ifPresent(retry::setMaxInterval); + overrider.withRetry(retry); + }; } /** diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java index a73ba5b6a5..fdd82774be 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java @@ -65,6 +65,8 @@ private Object convert(String raw, Class type) { return Integer.parseInt(raw); } else if (type == Long.class) { return Long.parseLong(raw); + } else if (type == Double.class) { + return Double.parseDouble(raw); } else if (type == Duration.class) { return Duration.parse(raw); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java index a042c7dff4..d8821ffee9 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java @@ -16,6 +16,7 @@ package io.javaoperatorsdk.operator.api.config.loader; import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; @@ -35,14 +36,16 @@ void returnsEmptyWhenNeitherEnvNorPropertyIsSet() { @Test void readsStringFromEnvVariable() { - var envProvider = new DefaultConfigProvider(k -> k.equals("JOSDK_TEST_STRING") ? "from-env" : null); + var envProvider = + new DefaultConfigProvider(k -> k.equals("JOSDK_TEST_STRING") ? "from-env" : null); assertThat(envProvider.getValue("josdk.test.string", String.class)).hasValue("from-env"); } @Test void envVariableKeyUsesUppercaseWithUnderscores() { // dots and hyphens both become underscores, key is uppercased - var envProvider = new DefaultConfigProvider(k -> k.equals("JOSDK_CACHE_SYNC_TIMEOUT") ? "PT10S" : null); + var envProvider = + new DefaultConfigProvider(k -> k.equals("JOSDK_CACHE_SYNC_TIMEOUT") ? "PT10S" : null); assertThat(envProvider.getValue("josdk.cache-sync.timeout", Duration.class)) .hasValue(Duration.ofSeconds(10)); } @@ -51,7 +54,8 @@ void envVariableKeyUsesUppercaseWithUnderscores() { void envVariableTakesPrecedenceOverSystemProperty() { System.setProperty("josdk.test.precedence", "from-sysprop"); try { - var envProvider = new DefaultConfigProvider(k -> k.equals("JOSDK_TEST_PRECEDENCE") ? "from-env" : null); + var envProvider = + new DefaultConfigProvider(k -> k.equals("JOSDK_TEST_PRECEDENCE") ? "from-env" : null); assertThat(envProvider.getValue("josdk.test.precedence", String.class)).hasValue("from-env"); } finally { System.clearProperty("josdk.test.precedence"); @@ -63,7 +67,8 @@ void fallsBackToSystemPropertyWhenEnvVariableAbsent() { System.setProperty("josdk.test.fallback", "from-sysprop"); try { var envProvider = new DefaultConfigProvider(k -> null); - assertThat(envProvider.getValue("josdk.test.fallback", String.class)).hasValue("from-sysprop"); + assertThat(envProvider.getValue("josdk.test.fallback", String.class)) + .hasValue("from-sysprop"); } finally { System.clearProperty("josdk.test.fallback"); } @@ -125,7 +130,7 @@ void throwsForUnsupportedType() { System.setProperty("josdk.test.unsupported", "value"); try { assertThatIllegalArgumentException() - .isThrownBy(() -> provider.getValue("josdk.test.unsupported", Double.class)) + .isThrownBy(() -> provider.getValue("josdk.test.unsupported", AtomicInteger.class)) .withMessageContaining("Unsupported config type"); } finally { System.clearProperty("josdk.test.unsupported"); From 78907fbfdc9b974117c77a9596464dcb3b84c08e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Feb 2026 16:15:42 +0100 Subject: [PATCH 07/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/sample/TomcatOperator.java | 11 ++++++----- .../operator/sample/TomcatReconciler.java | 5 +++-- .../operator/sample/WebappReconciler.java | 3 ++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java index bceaae8363..59ce9fd7b8 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -17,6 +17,7 @@ import java.io.IOException; +import io.javaoperatorsdk.operator.api.config.loader.ConfigLoader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.takes.facets.fork.FkRegex; @@ -28,13 +29,13 @@ public class TomcatOperator { - private static final Logger log = LoggerFactory.getLogger(TomcatOperator.class); - public static void main(String[] args) throws IOException { - Operator operator = new Operator(); - operator.register(new TomcatReconciler()); - operator.register(new WebappReconciler(operator.getKubernetesClient())); + Operator operator = new Operator(ConfigLoader.DEFAULT.applyConfigs()); + operator.register(new TomcatReconciler(), + ConfigLoader.DEFAULT.applyControllerConfigs(TomcatReconciler.TOMCAT_CONTROLLER_NAME)); + operator.register(new WebappReconciler(operator.getKubernetesClient()), + ConfigLoader.DEFAULT.applyControllerConfigs(WebappReconciler.WEBAPP_CONTROLLER_NAME)); operator.start(); new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD.")), 8080).start(Exit.NEVER); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index d2fa9a021f..874db639f6 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -35,10 +35,11 @@ @Dependent(type = DeploymentDependentResource.class), @Dependent(type = ServiceDependentResource.class) }) -@ControllerConfiguration +@ControllerConfiguration(name = TomcatReconciler.TOMCAT_CONTROLLER_NAME) public class TomcatReconciler implements Reconciler { - private final Logger log = LoggerFactory.getLogger(getClass()); + public static final String TOMCAT_CONTROLLER_NAME = "tomcat"; + private final Logger log = LoggerFactory.getLogger(getClass()); @Override public UpdateControl reconcile(Tomcat tomcat, Context context) { diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 5d362113ba..e5b1db0505 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -47,10 +47,11 @@ import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -@ControllerConfiguration +@ControllerConfiguration(name = WebappReconciler.WEBAPP_CONTROLLER_NAME) public class WebappReconciler implements Reconciler, Cleaner { private static final Logger log = LoggerFactory.getLogger(WebappReconciler.class); + public static final String WEBAPP_CONTROLLER_NAME = "webapp"; private final KubernetesClient kubernetesClient; From 10f8c430a0e11b5a3adb98e93767d094d15c5a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Feb 2026 17:18:05 +0100 Subject: [PATCH 08/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/sample/TomcatOperator.java | 14 +++++++------- .../operator/sample/TomcatReconciler.java | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java index 59ce9fd7b8..db3547cb05 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -17,25 +17,25 @@ import java.io.IOException; -import io.javaoperatorsdk.operator.api.config.loader.ConfigLoader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.takes.facets.fork.FkRegex; import org.takes.facets.fork.TkFork; import org.takes.http.Exit; import org.takes.http.FtBasic; import io.javaoperatorsdk.operator.Operator; +import io.javaoperatorsdk.operator.api.config.loader.ConfigLoader; public class TomcatOperator { public static void main(String[] args) throws IOException { Operator operator = new Operator(ConfigLoader.DEFAULT.applyConfigs()); - operator.register(new TomcatReconciler(), - ConfigLoader.DEFAULT.applyControllerConfigs(TomcatReconciler.TOMCAT_CONTROLLER_NAME)); - operator.register(new WebappReconciler(operator.getKubernetesClient()), - ConfigLoader.DEFAULT.applyControllerConfigs(WebappReconciler.WEBAPP_CONTROLLER_NAME)); + operator.register( + new TomcatReconciler(), + ConfigLoader.DEFAULT.applyControllerConfigs(TomcatReconciler.TOMCAT_CONTROLLER_NAME)); + operator.register( + new WebappReconciler(operator.getKubernetesClient()), + ConfigLoader.DEFAULT.applyControllerConfigs(WebappReconciler.WEBAPP_CONTROLLER_NAME)); operator.start(); new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD.")), 8080).start(Exit.NEVER); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 874db639f6..60a0d0bdc2 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -38,8 +38,8 @@ @ControllerConfiguration(name = TomcatReconciler.TOMCAT_CONTROLLER_NAME) public class TomcatReconciler implements Reconciler { - public static final String TOMCAT_CONTROLLER_NAME = "tomcat"; - private final Logger log = LoggerFactory.getLogger(getClass()); + public static final String TOMCAT_CONTROLLER_NAME = "tomcat"; + private final Logger log = LoggerFactory.getLogger(getClass()); @Override public UpdateControl reconcile(Tomcat tomcat, Context context) { From b42a4f4fd8fddd09f23549133b10e34d83e78c62 Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Mon, 23 Feb 2026 19:49:15 +0100 Subject: [PATCH 09/28] refactor: improve conversion code Signed-off-by: Chris Laprun --- .../config/loader/DefaultConfigProvider.java | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java index fdd82774be..1c2c7f126f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java @@ -38,17 +38,19 @@ public DefaultConfigProvider() { * variable takes precedence when both are set. */ @Override - @SuppressWarnings("unchecked") public Optional getValue(String key, Class type) { String raw = resolveRaw(key); if (raw == null) { return Optional.empty(); } - return Optional.of(type.cast(convert(raw, type))); + return Optional.of(convert(raw, type)); } private String resolveRaw(String key) { - String envKey = key.replace('.', '_').replace('-', '_').toUpperCase(); + if (key == null) { + return null; + } + String envKey = toEnvKey(key); String envValue = envLookup.apply(envKey); if (envValue != null) { return envValue; @@ -56,20 +58,27 @@ private String resolveRaw(String key) { return System.getProperty(key); } - private Object convert(String raw, Class type) { + private static String toEnvKey(String key) { + return key.trim().replace('.', '_').replace('-', '_').toUpperCase(); + } + + private static T convert(String raw, Class type) { + final Object converted; if (type == String.class) { - return raw; + converted = raw; } else if (type == Boolean.class) { - return Boolean.parseBoolean(raw); + converted = Boolean.parseBoolean(raw); } else if (type == Integer.class) { - return Integer.parseInt(raw); + converted = Integer.parseInt(raw); } else if (type == Long.class) { - return Long.parseLong(raw); + converted = Long.parseLong(raw); } else if (type == Double.class) { - return Double.parseDouble(raw); + converted = Double.parseDouble(raw); } else if (type == Duration.class) { - return Duration.parse(raw); + converted = Duration.parse(raw); + } else { + throw new IllegalArgumentException("Unsupported config type: " + type.getName()); } - throw new IllegalArgumentException("Unsupported config type: " + type.getName()); + return type.cast(converted); } } From aca713264497213c6fe677f4849c1082f276a8fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Feb 2026 20:40:28 +0100 Subject: [PATCH 10/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/loader/ConfigLoader.java | 9 ++++++--- .../api/config/loader/ConfigLoaderTest.java | 20 +++++++++++++------ .../operator/sample/TomcatOperator.java | 8 ++++---- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java index 679c096b99..eebb433fb2 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java @@ -27,7 +27,11 @@ public class ConfigLoader { - public static final ConfigLoader DEFAULT = new ConfigLoader(); + private static final ConfigLoader DEFAULT = new ConfigLoader(); + + public static ConfigLoader getDefault() { + return DEFAULT; + } public static final String DEFAULT_OPERATOR_KEY_PREFIX = "josdk."; public static final String DEFAULT_CONTROLLER_KEY_PREFIX = "josdk.controller."; @@ -179,7 +183,6 @@ Consumer> applyControllerConfigs(String cont if (retryStep != null) { consumer = consumer == null ? retryStep : consumer.andThen(retryStep); } - return consumer; } @@ -235,7 +238,7 @@ private Consumer buildConsumer(List> bindings, String consumer = consumer == null ? step : consumer.andThen(step); } } - return consumer; + return consumer == null ? o -> {} : consumer; } /** diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java index c31c27cafd..365543157a 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java @@ -44,9 +44,17 @@ public Optional getValue(String key, Class type) { // -- applyConfigs ----------------------------------------------------------- @Test - void applyConfigsReturnsNullWhenNothingConfigured() { + void applyConfigsReturnsNoOpWhenNothingConfigured() { var loader = new ConfigLoader(mapProvider(Map.of())); - assertThat(loader.applyConfigs()).isNull(); + var base = new BaseConfigurationService(null); + // consumer must be non-null and must leave all defaults unchanged + var consumer = loader.applyConfigs(); + assertThat(consumer).isNotNull(); + var result = ConfigurationService.newOverriddenConfigurationService(base, consumer); + assertThat(result.concurrentReconciliationThreads()) + .isEqualTo(base.concurrentReconciliationThreads()); + assertThat(result.concurrentWorkflowExecutorThreads()) + .isEqualTo(base.concurrentWorkflowExecutorThreads()); } @Test @@ -129,9 +137,9 @@ void applyConfigsOnlyAppliesPresentKeys() { // -- applyControllerConfigs ------------------------------------------------- @Test - void applyControllerConfigsReturnsNullWhenNothingConfigured() { + void applyControllerConfigsReturnsNoOpWhenNothingConfigured() { var loader = new ConfigLoader(mapProvider(Map.of())); - assertThat(loader.applyControllerConfigs("my-controller")).isNull(); + assertThat(loader.applyControllerConfigs("my-controller")).isNotNull(); } @Test @@ -163,8 +171,8 @@ void applyControllerConfigsIsolatesControllersByName() { // alpha gets a consumer (key found), beta gets a consumer (key found) assertThat(loader.applyControllerConfigs("alpha")).isNotNull(); assertThat(loader.applyControllerConfigs("beta")).isNotNull(); - // a controller with no configured keys gets null - assertThat(loader.applyControllerConfigs("gamma")).isNull(); + // a controller with no configured keys still gets a non-null no-op consumer + assertThat(loader.applyControllerConfigs("gamma")).isNotNull(); } @Test diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java index db3547cb05..b29be9294a 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -28,14 +28,14 @@ public class TomcatOperator { public static void main(String[] args) throws IOException { - - Operator operator = new Operator(ConfigLoader.DEFAULT.applyConfigs()); + var configLoader = ConfigLoader.getDefault(); + Operator operator = new Operator(configLoader.applyConfigs()); operator.register( new TomcatReconciler(), - ConfigLoader.DEFAULT.applyControllerConfigs(TomcatReconciler.TOMCAT_CONTROLLER_NAME)); + configLoader.applyControllerConfigs(TomcatReconciler.TOMCAT_CONTROLLER_NAME)); operator.register( new WebappReconciler(operator.getKubernetesClient()), - ConfigLoader.DEFAULT.applyControllerConfigs(WebappReconciler.WEBAPP_CONTROLLER_NAME)); + configLoader.applyControllerConfigs(WebappReconciler.WEBAPP_CONTROLLER_NAME)); operator.start(); new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD.")), 8080).start(Exit.NEVER); From cf21fbab6efe09d85cab72ddf76b6801fd828d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Feb 2026 20:41:41 +0100 Subject: [PATCH 11/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../io/javaoperatorsdk/operator/sample/TomcatOperator.java | 5 ++--- .../io/javaoperatorsdk/operator/sample/TomcatReconciler.java | 4 ++-- .../io/javaoperatorsdk/operator/sample/WebappReconciler.java | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java index b29be9294a..c597956319 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -31,11 +31,10 @@ public static void main(String[] args) throws IOException { var configLoader = ConfigLoader.getDefault(); Operator operator = new Operator(configLoader.applyConfigs()); operator.register( - new TomcatReconciler(), - configLoader.applyControllerConfigs(TomcatReconciler.TOMCAT_CONTROLLER_NAME)); + new TomcatReconciler(), configLoader.applyControllerConfigs(TomcatReconciler.NAME)); operator.register( new WebappReconciler(operator.getKubernetesClient()), - configLoader.applyControllerConfigs(WebappReconciler.WEBAPP_CONTROLLER_NAME)); + configLoader.applyControllerConfigs(WebappReconciler.NAME)); operator.start(); new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD.")), 8080).start(Exit.NEVER); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 60a0d0bdc2..6bb454eb13 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -35,10 +35,10 @@ @Dependent(type = DeploymentDependentResource.class), @Dependent(type = ServiceDependentResource.class) }) -@ControllerConfiguration(name = TomcatReconciler.TOMCAT_CONTROLLER_NAME) +@ControllerConfiguration(name = TomcatReconciler.NAME) public class TomcatReconciler implements Reconciler { - public static final String TOMCAT_CONTROLLER_NAME = "tomcat"; + public static final String NAME = "tomcat"; private final Logger log = LoggerFactory.getLogger(getClass()); @Override diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index e5b1db0505..32d32f0a5b 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -47,11 +47,11 @@ import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -@ControllerConfiguration(name = WebappReconciler.WEBAPP_CONTROLLER_NAME) +@ControllerConfiguration(name = WebappReconciler.NAME) public class WebappReconciler implements Reconciler, Cleaner { private static final Logger log = LoggerFactory.getLogger(WebappReconciler.class); - public static final String WEBAPP_CONTROLLER_NAME = "webapp"; + public static final String NAME = "webapp"; private final KubernetesClient kubernetesClient; From e4411df86cbbdaa737c1071e3739e4dfd37d75ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Feb 2026 21:01:43 +0100 Subject: [PATCH 12/28] Unit test to check if we cover all the overrider methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/loader/ConfigLoader.java | 55 +++++------ .../api/config/loader/ConfigLoaderTest.java | 94 ++++++++++++++++++- 2 files changed, 118 insertions(+), 31 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java index eebb433fb2..438eb30c4f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java @@ -50,7 +50,7 @@ public static ConfigLoader getDefault() { // included. Complex objects (KubernetesClient, ExecutorService, …) must be // configured programmatically and are intentionally omitted. // --------------------------------------------------------------------------- - private static final List> OPERATOR_BINDINGS = + static final List> OPERATOR_BINDINGS = List.of( new ConfigBinding<>( "check-crd", @@ -106,35 +106,30 @@ public static ConfigLoader getDefault() { // The key used at runtime is built as: // CONTROLLER_KEY_PREFIX + controllerName + "." + // --------------------------------------------------------------------------- - private static final List, ?>> - CONTROLLER_BINDINGS = - List.of( - new ConfigBinding<>( - "finalizer", String.class, ControllerConfigurationOverrider::withFinalizer), - new ConfigBinding<>( - "generation-aware", - Boolean.class, - ControllerConfigurationOverrider::withGenerationAware), - new ConfigBinding<>( - "label-selector", - String.class, - ControllerConfigurationOverrider::withLabelSelector), - new ConfigBinding<>( - "max-reconciliation-interval", - Duration.class, - ControllerConfigurationOverrider::withReconciliationMaxInterval), - new ConfigBinding<>( - "field-manager", - String.class, - ControllerConfigurationOverrider::withFieldManager), - new ConfigBinding<>( - "trigger-reconciler-on-all-events", - Boolean.class, - ControllerConfigurationOverrider::withTriggerReconcilerOnAllEvents), - new ConfigBinding<>( - "informer-list-limit", - Long.class, - ControllerConfigurationOverrider::withInformerListLimit)); + static final List, ?>> CONTROLLER_BINDINGS = + List.of( + new ConfigBinding<>( + "finalizer", String.class, ControllerConfigurationOverrider::withFinalizer), + new ConfigBinding<>( + "generation-aware", + Boolean.class, + ControllerConfigurationOverrider::withGenerationAware), + new ConfigBinding<>( + "label-selector", String.class, ControllerConfigurationOverrider::withLabelSelector), + new ConfigBinding<>( + "max-reconciliation-interval", + Duration.class, + ControllerConfigurationOverrider::withReconciliationMaxInterval), + new ConfigBinding<>( + "field-manager", String.class, ControllerConfigurationOverrider::withFieldManager), + new ConfigBinding<>( + "trigger-reconciler-on-all-events", + Boolean.class, + ControllerConfigurationOverrider::withTriggerReconcilerOnAllEvents), + new ConfigBinding<>( + "informer-list-limit", + Long.class, + ControllerConfigurationOverrider::withInformerListLimit)); private final ConfigProvider configProvider; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java index 365543157a..765ddf3328 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java @@ -17,14 +17,19 @@ import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import io.javaoperatorsdk.operator.api.config.BaseConfigurationService; import io.javaoperatorsdk.operator.api.config.ConfigurationService; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import static org.assertj.core.api.Assertions.assertThat; @@ -200,7 +205,94 @@ public Optional getValue(String key, Class type) { "josdk.controller.ctrl.informer-list-limit"); } - // -- key prefix constants --------------------------------------------------- + // -- binding coverage ------------------------------------------------------- + + /** + * Supported scalar types that DefaultConfigProvider can parse from a string. Every binding's type + * must be one of these. + */ + private static final Set> SUPPORTED_TYPES = + Set.of( + Boolean.class, + boolean.class, + Integer.class, + int.class, + Long.class, + long.class, + Double.class, + double.class, + Duration.class, + String.class); + + @Test + void operatorBindingsCoverAllSingleScalarSettersOnConfigurationServiceOverrider() { + Set expectedSetters = + Arrays.stream(ConfigurationServiceOverrider.class.getMethods()) + .filter(m -> m.getParameterCount() == 1) + .filter(m -> SUPPORTED_TYPES.contains(m.getParameterTypes()[0])) + .filter(m -> m.getReturnType() == ConfigurationServiceOverrider.class) + .map(java.lang.reflect.Method::getName) + .collect(Collectors.toSet()); + + Set boundMethodNames = + ConfigLoader.OPERATOR_BINDINGS.stream() + .flatMap( + b -> + Arrays.stream(ConfigurationServiceOverrider.class.getMethods()) + .filter(m -> m.getParameterCount() == 1) + .filter(m -> isTypeCompatible(m.getParameterTypes()[0], b.type())) + .filter(m -> m.getReturnType() == ConfigurationServiceOverrider.class) + .map(java.lang.reflect.Method::getName)) + .collect(Collectors.toSet()); + + assertThat(boundMethodNames) + .as("Every scalar setter on ConfigurationServiceOverrider must be covered by a binding") + .containsExactlyInAnyOrderElementsOf(expectedSetters); + } + + @Test + void controllerBindingsCoverAllSingleScalarSettersOnControllerConfigurationOverrider() { + Set expectedSetters = + Arrays.stream(ControllerConfigurationOverrider.class.getMethods()) + .filter(m -> m.getParameterCount() == 1) + .filter(m -> SUPPORTED_TYPES.contains(m.getParameterTypes()[0])) + .filter(m -> m.getReturnType() == ControllerConfigurationOverrider.class) + .filter(m -> m.getAnnotation(Deprecated.class) == null) + .map(java.lang.reflect.Method::getName) + .collect(Collectors.toSet()); + + Set boundMethodNames = + ConfigLoader.CONTROLLER_BINDINGS.stream() + .flatMap( + b -> + Arrays.stream(ControllerConfigurationOverrider.class.getMethods()) + .filter(m -> m.getParameterCount() == 1) + .filter(m -> isTypeCompatible(m.getParameterTypes()[0], b.type())) + .filter(m -> m.getReturnType() == ControllerConfigurationOverrider.class) + .filter(m -> m.getAnnotation(Deprecated.class) == null) + .map(java.lang.reflect.Method::getName)) + .collect(Collectors.toSet()); + + assertThat(boundMethodNames) + .as( + "Every scalar setter on ControllerConfigurationOverrider should be covered by a" + + " binding") + .containsExactlyInAnyOrderElementsOf(expectedSetters); + } + + /** Returns true when the two types are the same or one is the boxed/unboxed form of the other. */ + private static boolean isTypeCompatible(Class methodParam, Class bindingType) { + if (methodParam == bindingType) return true; + if (methodParam == boolean.class && bindingType == Boolean.class) return true; + if (methodParam == Boolean.class && bindingType == boolean.class) return true; + if (methodParam == int.class && bindingType == Integer.class) return true; + if (methodParam == Integer.class && bindingType == int.class) return true; + if (methodParam == long.class && bindingType == Long.class) return true; + if (methodParam == Long.class && bindingType == long.class) return true; + if (methodParam == double.class && bindingType == Double.class) return true; + if (methodParam == Double.class && bindingType == double.class) return true; + return false; + } @Test void operatorKeyPrefixIsJosdkDot() { From 823a6b99efbc0413e09aa0ff913b36cc52771815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Feb 2026 21:07:45 +0100 Subject: [PATCH 13/28] small cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/loader/ConfigLoaderTest.java | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java index 765ddf3328..7e8d8c1db6 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java @@ -46,8 +46,6 @@ public Optional getValue(String key, Class type) { }; } - // -- applyConfigs ----------------------------------------------------------- - @Test void applyConfigsReturnsNoOpWhenNothingConfigured() { var loader = new ConfigLoader(mapProvider(Map.of())); @@ -205,6 +203,16 @@ public Optional getValue(String key, Class type) { "josdk.controller.ctrl.informer-list-limit"); } + @Test + void operatorKeyPrefixIsJosdkDot() { + assertThat(ConfigLoader.DEFAULT_OPERATOR_KEY_PREFIX).isEqualTo("josdk."); + } + + @Test + void controllerKeyPrefixIsJosdkControllerDot() { + assertThat(ConfigLoader.DEFAULT_CONTROLLER_KEY_PREFIX).isEqualTo("josdk.controller."); + } + // -- binding coverage ------------------------------------------------------- /** @@ -293,14 +301,4 @@ private static boolean isTypeCompatible(Class methodParam, Class bindingTy if (methodParam == Double.class && bindingType == double.class) return true; return false; } - - @Test - void operatorKeyPrefixIsJosdkDot() { - assertThat(ConfigLoader.DEFAULT_OPERATOR_KEY_PREFIX).isEqualTo("josdk."); - } - - @Test - void controllerKeyPrefixIsJosdkControllerDot() { - assertThat(ConfigLoader.DEFAULT_CONTROLLER_KEY_PREFIX).isEqualTo("josdk.controller."); - } } From a6ff350f791eeb146312fc9d8b56e883fb1a8945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Feb 2026 21:15:54 +0100 Subject: [PATCH 14/28] Update operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../operator/api/config/loader/ConfigLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java index 438eb30c4f..ec372fa187 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java @@ -222,7 +222,7 @@ private Consumer> bu * @param bindings the predefined bindings to check * @param keyPrefix when non-null the key stored in the binding is treated as a suffix and this * prefix is prepended before the lookup - * @return a consumer that applies all found values, or {@code null} if none were found + * @return a consumer that applies all found values, or a no-op consumer if none were found */ private Consumer buildConsumer(List> bindings, String keyPrefix) { Consumer consumer = null; From 4cc92862f449419a91dc347cc88d3505a0988c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Mon, 23 Feb 2026 21:18:33 +0100 Subject: [PATCH 15/28] javadoc fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../api/config/loader/ConfigLoader.java | 2 +- .../config/loader/DefaultConfigProvider.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java index ec372fa187..96da618907 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java @@ -150,7 +150,7 @@ public ConfigLoader( /** * Returns a {@link Consumer} that applies every operator-level property found in the {@link - * ConfigProvider} to the given {@link ConfigurationServiceOverrider}. Returns {@code null} when + * ConfigProvider} to the given {@link ConfigurationServiceOverrider}. Returns no-op consumer when * no binding has a matching value, preserving the previous behavior. */ public Consumer applyConfigs() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java index 1c2c7f126f..06d97f709b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java @@ -19,6 +19,25 @@ import java.util.Optional; import java.util.function.Function; +/** + * A {@link ConfigProvider} that resolves configuration values from environment variables and Java + * system properties. + * + *

For a given key, lookup proceeds as follows: + * + *

    + *
  1. The key is converted to an environment variable name by replacing dots and hyphens with + * underscores and converting to upper case (e.g. {@code josdk.cache-sync.timeout} → {@code + * JOSDK_CACHE_SYNC_TIMEOUT}). If an environment variable with that name is set, its value is + * used. + *
  2. If no matching environment variable is found, the key is looked up as a Java system + * property (via {@link System#getProperty(String)}) using the original key name. + *
+ * + *

Environment variables take precedence over system properties when both are set. Supported + * value types are: {@link String}, {@link Boolean}, {@link Integer}, {@link Long}, {@link Double}, + * and {@link java.time.Duration} (ISO-8601 format, e.g. {@code PT30S}). + */ public class DefaultConfigProvider implements ConfigProvider { private final Function envLookup; From 9e9fd366aa682b9ddab6b839995af5adcda99376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 24 Feb 2026 08:58:17 +0100 Subject: [PATCH 16/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../javaoperatorsdk/operator}/config/loader/ConfigBinding.java | 2 +- .../javaoperatorsdk/operator}/config/loader/ConfigLoader.java | 2 +- .../javaoperatorsdk/operator}/config/loader/ConfigProvider.java | 2 +- .../operator}/config/loader/DefaultConfigProvider.java | 2 +- .../operator}/config/loader/ConfigBindingTest.java | 2 +- .../operator}/config/loader/ConfigLoaderTest.java | 2 +- .../operator}/config/loader/DefaultConfigProviderTest.java | 2 +- .../java/io/javaoperatorsdk/operator/sample/TomcatOperator.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename {operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api => operator-framework/src/main/java/io/javaoperatorsdk/operator}/config/loader/ConfigBinding.java (96%) rename {operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api => operator-framework/src/main/java/io/javaoperatorsdk/operator}/config/loader/ConfigLoader.java (99%) rename {operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api => operator-framework/src/main/java/io/javaoperatorsdk/operator}/config/loader/ConfigProvider.java (95%) rename {operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api => operator-framework/src/main/java/io/javaoperatorsdk/operator}/config/loader/DefaultConfigProvider.java (98%) rename {operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api => operator-framework/src/test/java/io/javaoperatorsdk/operator}/config/loader/ConfigBindingTest.java (95%) rename {operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api => operator-framework/src/test/java/io/javaoperatorsdk/operator}/config/loader/ConfigLoaderTest.java (99%) rename {operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api => operator-framework/src/test/java/io/javaoperatorsdk/operator}/config/loader/DefaultConfigProviderTest.java (98%) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBinding.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigBinding.java similarity index 96% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBinding.java rename to operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigBinding.java index 069932b189..7cb508b2f1 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBinding.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigBinding.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.javaoperatorsdk.operator.api.config.loader; +package io.javaoperatorsdk.operator.config.loader; import java.util.function.BiConsumer; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java similarity index 99% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java rename to operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java index 96da618907..22257d5701 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.javaoperatorsdk.operator.api.config.loader; +package io.javaoperatorsdk.operator.config.loader; import java.time.Duration; import java.util.List; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigProvider.java similarity index 95% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java rename to operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigProvider.java index 9279439d68..000131ff3b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.javaoperatorsdk.operator.api.config.loader; +package io.javaoperatorsdk.operator.config.loader; import java.util.Optional; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProvider.java similarity index 98% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java rename to operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProvider.java index 06d97f709b..70628a8b13 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.javaoperatorsdk.operator.api.config.loader; +package io.javaoperatorsdk.operator.config.loader; import java.time.Duration; import java.util.Optional; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBindingTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigBindingTest.java similarity index 95% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBindingTest.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigBindingTest.java index 6a1c7aeecd..384ebb600c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBindingTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigBindingTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.javaoperatorsdk.operator.api.config.loader; +package io.javaoperatorsdk.operator.config.loader; import java.util.ArrayList; import java.util.List; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java similarity index 99% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java index 7e8d8c1db6..88b0d0b7ca 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.javaoperatorsdk.operator.api.config.loader; +package io.javaoperatorsdk.operator.config.loader; import java.time.Duration; import java.util.ArrayList; diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProviderTest.java similarity index 98% rename from operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProviderTest.java index d8821ffee9..06a3c8489c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProviderTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.javaoperatorsdk.operator.api.config.loader; +package io.javaoperatorsdk.operator.config.loader; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java index c597956319..bb37892c11 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -23,7 +23,7 @@ import org.takes.http.FtBasic; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.api.config.loader.ConfigLoader; +import io.javaoperatorsdk.operator.config.loader.ConfigLoader; public class TomcatOperator { From 05ab4bb36e32882caa0e24a52db58cce06adec02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 24 Feb 2026 17:39:35 +0100 Subject: [PATCH 17/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/config/loader/ConfigLoader.java | 9 +- .../config/loader/DefaultConfigProvider.java | 103 -------------- .../loader/provider/ConfigValueConverter.java | 51 +++++++ .../loader/provider/EnvVarConfigProvider.java | 60 ++++++++ .../provider/PrirityListConfigProvider.java | 45 ++++++ .../provider/PropertiesConfigProvider.java | 74 ++++++++++ .../SystemPropertyConfigProvider.java | 53 +++++++ .../config/loader/ConfigLoaderTest.java | 4 +- .../provider/EnvVarConfigProviderTest.java | 62 +++++++++ .../PriorityListConfigProviderTest.java | 67 +++++++++ .../PropertiesConfigProviderTest.java | 129 ++++++++++++++++++ .../SystemPropertyConfigProviderTest.java} | 57 ++------ 12 files changed, 564 insertions(+), 150 deletions(-) delete mode 100644 operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProvider.java create mode 100644 operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/ConfigValueConverter.java create mode 100644 operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/EnvVarConfigProvider.java create mode 100644 operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PrirityListConfigProvider.java create mode 100644 operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java create mode 100644 operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProvider.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/EnvVarConfigProviderTest.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProviderTest.java rename operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/{DefaultConfigProviderTest.java => provider/SystemPropertyConfigProviderTest.java} (61%) diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java index 22257d5701..9d04c3154e 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java @@ -23,6 +23,9 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; +import io.javaoperatorsdk.operator.config.loader.provider.EnvVarConfigProvider; +import io.javaoperatorsdk.operator.config.loader.provider.PrirityListConfigProvider; +import io.javaoperatorsdk.operator.config.loader.provider.SystemPropertyConfigProvider; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; public class ConfigLoader { @@ -134,7 +137,11 @@ public static ConfigLoader getDefault() { private final ConfigProvider configProvider; public ConfigLoader() { - this(new DefaultConfigProvider(), DEFAULT_CONTROLLER_KEY_PREFIX, DEFAULT_OPERATOR_KEY_PREFIX); + this( + new PrirityListConfigProvider( + List.of(new EnvVarConfigProvider(), new SystemPropertyConfigProvider())), + DEFAULT_CONTROLLER_KEY_PREFIX, + DEFAULT_OPERATOR_KEY_PREFIX); } public ConfigLoader(ConfigProvider configProvider) { diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProvider.java deleted file mode 100644 index 70628a8b13..0000000000 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProvider.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Java Operator SDK Authors - * - * Licensed 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 io.javaoperatorsdk.operator.config.loader; - -import java.time.Duration; -import java.util.Optional; -import java.util.function.Function; - -/** - * A {@link ConfigProvider} that resolves configuration values from environment variables and Java - * system properties. - * - *

For a given key, lookup proceeds as follows: - * - *

    - *
  1. The key is converted to an environment variable name by replacing dots and hyphens with - * underscores and converting to upper case (e.g. {@code josdk.cache-sync.timeout} → {@code - * JOSDK_CACHE_SYNC_TIMEOUT}). If an environment variable with that name is set, its value is - * used. - *
  2. If no matching environment variable is found, the key is looked up as a Java system - * property (via {@link System#getProperty(String)}) using the original key name. - *
- * - *

Environment variables take precedence over system properties when both are set. Supported - * value types are: {@link String}, {@link Boolean}, {@link Integer}, {@link Long}, {@link Double}, - * and {@link java.time.Duration} (ISO-8601 format, e.g. {@code PT30S}). - */ -public class DefaultConfigProvider implements ConfigProvider { - - private final Function envLookup; - - public DefaultConfigProvider() { - this(System::getenv); - } - - DefaultConfigProvider(Function envLookup) { - this.envLookup = envLookup; - } - - /** - * Looks up {@code key} first as an environment variable (dots and hyphens replaced by - * underscores, uppercased, e.g. {@code josdk.cache.sync.timeout} → {@code - * JOSDK_CACHE_SYNC_TIMEOUT}), then as a system property with the key as-is. The environment - * variable takes precedence when both are set. - */ - @Override - public Optional getValue(String key, Class type) { - String raw = resolveRaw(key); - if (raw == null) { - return Optional.empty(); - } - return Optional.of(convert(raw, type)); - } - - private String resolveRaw(String key) { - if (key == null) { - return null; - } - String envKey = toEnvKey(key); - String envValue = envLookup.apply(envKey); - if (envValue != null) { - return envValue; - } - return System.getProperty(key); - } - - private static String toEnvKey(String key) { - return key.trim().replace('.', '_').replace('-', '_').toUpperCase(); - } - - private static T convert(String raw, Class type) { - final Object converted; - if (type == String.class) { - converted = raw; - } else if (type == Boolean.class) { - converted = Boolean.parseBoolean(raw); - } else if (type == Integer.class) { - converted = Integer.parseInt(raw); - } else if (type == Long.class) { - converted = Long.parseLong(raw); - } else if (type == Double.class) { - converted = Double.parseDouble(raw); - } else if (type == Duration.class) { - converted = Duration.parse(raw); - } else { - throw new IllegalArgumentException("Unsupported config type: " + type.getName()); - } - return type.cast(converted); - } -} diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/ConfigValueConverter.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/ConfigValueConverter.java new file mode 100644 index 0000000000..09c5c3fcf2 --- /dev/null +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/ConfigValueConverter.java @@ -0,0 +1,51 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; + +import java.time.Duration; + +/** Utility for converting raw string config values to typed instances. */ +final class ConfigValueConverter { + + private ConfigValueConverter() {} + + /** + * Converts {@code raw} to an instance of {@code type}. Supported types: {@link String}, {@link + * Boolean}, {@link Integer}, {@link Long}, {@link Double}, and {@link Duration} (ISO-8601 format, + * e.g. {@code PT30S}). + * + * @throws IllegalArgumentException if {@code type} is not supported + */ + public static T convert(String raw, Class type) { + final Object converted; + if (type == String.class) { + converted = raw; + } else if (type == Boolean.class) { + converted = Boolean.parseBoolean(raw); + } else if (type == Integer.class) { + converted = Integer.parseInt(raw); + } else if (type == Long.class) { + converted = Long.parseLong(raw); + } else if (type == Double.class) { + converted = Double.parseDouble(raw); + } else if (type == Duration.class) { + converted = Duration.parse(raw); + } else { + throw new IllegalArgumentException("Unsupported config type: " + type.getName()); + } + return type.cast(converted); + } +} diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/EnvVarConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/EnvVarConfigProvider.java new file mode 100644 index 0000000000..916ee6391d --- /dev/null +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/EnvVarConfigProvider.java @@ -0,0 +1,60 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; + +import java.util.Optional; +import java.util.function.Function; + +import io.javaoperatorsdk.operator.config.loader.ConfigProvider; + +/** + * A {@link ConfigProvider} that resolves configuration values from environment variables. + * + *

The key is converted to an environment variable name by replacing dots and hyphens with + * underscores and converting to upper case (e.g. {@code josdk.cache-sync.timeout} → {@code + * JOSDK_CACHE_SYNC_TIMEOUT}). + * + *

Supported value types are: {@link String}, {@link Boolean}, {@link Integer}, {@link Long}, + * {@link Double}, and {@link java.time.Duration} (ISO-8601 format, e.g. {@code PT30S}). + */ +public class EnvVarConfigProvider implements ConfigProvider { + + private final Function envLookup; + + public EnvVarConfigProvider() { + this(System::getenv); + } + + EnvVarConfigProvider(Function envLookup) { + this.envLookup = envLookup; + } + + @Override + public Optional getValue(String key, Class type) { + if (key == null) { + return Optional.empty(); + } + String raw = envLookup.apply(toEnvKey(key)); + if (raw == null) { + return Optional.empty(); + } + return Optional.of(ConfigValueConverter.convert(raw, type)); + } + + static String toEnvKey(String key) { + return key.trim().replace('.', '_').replace('-', '_').toUpperCase(); + } +} diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PrirityListConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PrirityListConfigProvider.java new file mode 100644 index 0000000000..8d43f2e0ce --- /dev/null +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PrirityListConfigProvider.java @@ -0,0 +1,45 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; + +import java.util.List; +import java.util.Optional; + +import io.javaoperatorsdk.operator.config.loader.ConfigProvider; + +/** + * A {@link ConfigProvider} that delegates to an ordered list of providers. Providers are queried in + * list order; the first non-empty result wins. + */ +public class PrirityListConfigProvider implements ConfigProvider { + + private final List providers; + + public PrirityListConfigProvider(List providers) { + this.providers = List.copyOf(providers); + } + + @Override + public Optional getValue(String key, Class type) { + for (ConfigProvider provider : providers) { + Optional value = provider.getValue(key, type); + if (value.isPresent()) { + return value; + } + } + return Optional.empty(); + } +} diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java new file mode 100644 index 0000000000..01ef5b4b03 --- /dev/null +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java @@ -0,0 +1,74 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.Properties; + +import io.javaoperatorsdk.operator.config.loader.ConfigProvider; + +/** + * A {@link ConfigProvider} that resolves configuration values from a {@link Properties} file. + * + *

Keys are looked up as-is against the loaded properties. Supported value types are: {@link + * String}, {@link Boolean}, {@link Integer}, {@link Long}, {@link Double}, and {@link + * java.time.Duration} (ISO-8601 format, e.g. {@code PT30S}). + */ +public class PropertiesConfigProvider implements ConfigProvider { + + private final Properties properties; + + /** + * Loads properties from the given file path. + * + * @throws UncheckedIOException if the file cannot be read + */ + public PropertiesConfigProvider(Path path) { + this.properties = load(path); + } + + /** Uses the supplied {@link Properties} instance directly. */ + public PropertiesConfigProvider(Properties properties) { + this.properties = properties; + } + + @Override + public Optional getValue(String key, Class type) { + if (key == null) { + return Optional.empty(); + } + String raw = properties.getProperty(key); + if (raw == null) { + return Optional.empty(); + } + return Optional.of(ConfigValueConverter.convert(raw, type)); + } + + private static Properties load(Path path) { + try (InputStream in = Files.newInputStream(path)) { + Properties props = new Properties(); + props.load(in); + return props; + } catch (IOException e) { + throw new UncheckedIOException("Failed to load config properties from " + path, e); + } + } +} diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProvider.java new file mode 100644 index 0000000000..f777eb378f --- /dev/null +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; + +import java.util.Optional; +import java.util.function.Function; + +import io.javaoperatorsdk.operator.config.loader.ConfigProvider; + +/** + * A {@link ConfigProvider} that resolves configuration values from Java system properties via + * {@link System#getProperty(String)}, using the key as-is. + * + *

Supported value types are: {@link String}, {@link Boolean}, {@link Integer}, {@link Long}, + * {@link Double}, and {@link java.time.Duration} (ISO-8601 format, e.g. {@code PT30S}). + */ +public class SystemPropertyConfigProvider implements ConfigProvider { + + private final Function propertyLookup; + + public SystemPropertyConfigProvider() { + this(System::getProperty); + } + + SystemPropertyConfigProvider(Function propertyLookup) { + this.propertyLookup = propertyLookup; + } + + @Override + public Optional getValue(String key, Class type) { + if (key == null) { + return Optional.empty(); + } + String raw = propertyLookup.apply(key); + if (raw == null) { + return Optional.empty(); + } + return Optional.of(ConfigValueConverter.convert(raw, type)); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java index 88b0d0b7ca..460eebcbc9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java @@ -216,8 +216,8 @@ void controllerKeyPrefixIsJosdkControllerDot() { // -- binding coverage ------------------------------------------------------- /** - * Supported scalar types that DefaultConfigProvider can parse from a string. Every binding's type - * must be one of these. + * Supported scalar types that PrirityListConfigProvider can parse from a string. Every binding's + * type must be one of these. */ private static final Set> SUPPORTED_TYPES = Set.of( diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/EnvVarConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/EnvVarConfigProviderTest.java new file mode 100644 index 0000000000..3a4d07dd60 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/EnvVarConfigProviderTest.java @@ -0,0 +1,62 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class EnvVarConfigProviderTest { + + @Test + void returnsEmptyWhenEnvVariableAbsent() { + var provider = new EnvVarConfigProvider(k -> null); + assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty(); + } + + @Test + void returnsEmptyForNullKey() { + var provider = new EnvVarConfigProvider(k -> "value"); + assertThat(provider.getValue(null, String.class)).isEmpty(); + } + + @Test + void readsStringFromEnvVariable() { + var provider = new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_STRING") ? "from-env" : null); + assertThat(provider.getValue("josdk.test.string", String.class)).hasValue("from-env"); + } + + @Test + void convertsDotsAndHyphensToUnderscoresAndUppercases() { + var provider = + new EnvVarConfigProvider(k -> k.equals("JOSDK_CACHE_SYNC_TIMEOUT") ? "PT10S" : null); + assertThat(provider.getValue("josdk.cache-sync.timeout", Duration.class)) + .hasValue(Duration.ofSeconds(10)); + } + + @Test + void throwsForUnsupportedType() { + var provider = + new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_UNSUPPORTED") ? "value" : null); + assertThatIllegalArgumentException() + .isThrownBy(() -> provider.getValue("josdk.test.unsupported", AtomicInteger.class)) + .withMessageContaining("Unsupported config type"); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java new file mode 100644 index 0000000000..b5fa8d3a24 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java @@ -0,0 +1,67 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PriorityListConfigProviderTest { + + @Test + void returnsEmptyWhenAllProvidersReturnEmpty() { + var provider = + new PrirityListConfigProvider( + List.of( + new EnvVarConfigProvider(k -> null), new SystemPropertyConfigProvider(k -> null))); + assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty(); + } + + @Test + void firstProviderWins() { + var provider = + new PrirityListConfigProvider( + List.of( + new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_KEY") ? "first" : null), + new SystemPropertyConfigProvider( + k -> k.equals("josdk.test.key") ? "second" : null))); + assertThat(provider.getValue("josdk.test.key", String.class)).hasValue("first"); + } + + @Test + void fallsBackToLaterProviderWhenEarlierReturnsEmpty() { + var provider = + new PrirityListConfigProvider( + List.of( + new EnvVarConfigProvider(k -> null), + new SystemPropertyConfigProvider( + k -> k.equals("josdk.test.key") ? "from-second" : null))); + assertThat(provider.getValue("josdk.test.key", String.class)).hasValue("from-second"); + } + + @Test + void respectsOrderWithThreeProviders() { + var first = new EnvVarConfigProvider(k -> null); + var second = + new SystemPropertyConfigProvider(k -> k.equals("josdk.test.key") ? "from-second" : null); + var third = new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_KEY") ? "from-third" : null); + + var provider = new PrirityListConfigProvider(List.of(first, second, third)); + assertThat(provider.getValue("josdk.test.key", String.class)).hasValue("from-second"); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProviderTest.java new file mode 100644 index 0000000000..c44534eb3a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProviderTest.java @@ -0,0 +1,129 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class PropertiesConfigProviderTest { + + // -- Properties constructor ------------------------------------------------- + + @Test + void returnsEmptyWhenKeyAbsent() { + var provider = new PropertiesConfigProvider(new Properties()); + assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty(); + } + + @Test + void returnsEmptyForNullKey() { + var props = new Properties(); + props.setProperty("josdk.test.key", "value"); + var provider = new PropertiesConfigProvider(props); + assertThat(provider.getValue(null, String.class)).isEmpty(); + } + + @Test + void readsString() { + var props = new Properties(); + props.setProperty("josdk.test.string", "hello"); + var provider = new PropertiesConfigProvider(props); + assertThat(provider.getValue("josdk.test.string", String.class)).hasValue("hello"); + } + + @Test + void readsBoolean() { + var props = new Properties(); + props.setProperty("josdk.test.bool", "true"); + var provider = new PropertiesConfigProvider(props); + assertThat(provider.getValue("josdk.test.bool", Boolean.class)).hasValue(true); + } + + @Test + void readsInteger() { + var props = new Properties(); + props.setProperty("josdk.test.integer", "42"); + var provider = new PropertiesConfigProvider(props); + assertThat(provider.getValue("josdk.test.integer", Integer.class)).hasValue(42); + } + + @Test + void readsLong() { + var props = new Properties(); + props.setProperty("josdk.test.long", "123456789"); + var provider = new PropertiesConfigProvider(props); + assertThat(provider.getValue("josdk.test.long", Long.class)).hasValue(123456789L); + } + + @Test + void readsDouble() { + var props = new Properties(); + props.setProperty("josdk.test.double", "3.14"); + var provider = new PropertiesConfigProvider(props); + assertThat(provider.getValue("josdk.test.double", Double.class)).hasValue(3.14); + } + + @Test + void readsDuration() { + var props = new Properties(); + props.setProperty("josdk.test.duration", "PT30S"); + var provider = new PropertiesConfigProvider(props); + assertThat(provider.getValue("josdk.test.duration", Duration.class)) + .hasValue(Duration.ofSeconds(30)); + } + + @Test + void throwsForUnsupportedType() { + var props = new Properties(); + props.setProperty("josdk.test.unsupported", "value"); + var provider = new PropertiesConfigProvider(props); + assertThatIllegalArgumentException() + .isThrownBy(() -> provider.getValue("josdk.test.unsupported", AtomicInteger.class)) + .withMessageContaining("Unsupported config type"); + } + + // -- Path constructor ------------------------------------------------------- + + @Test + void loadsFromFile(@TempDir Path dir) throws IOException { + Path file = dir.resolve("test.properties"); + Files.writeString(file, "josdk.test.string=from-file\njosdk.test.integer=7\n"); + + var provider = new PropertiesConfigProvider(file); + assertThat(provider.getValue("josdk.test.string", String.class)).hasValue("from-file"); + assertThat(provider.getValue("josdk.test.integer", Integer.class)).hasValue(7); + } + + @Test + void throwsUncheckedIOExceptionForMissingFile(@TempDir Path dir) { + Path missing = dir.resolve("does-not-exist.properties"); + assertThatExceptionOfType(UncheckedIOException.class) + .isThrownBy(() -> new PropertiesConfigProvider(missing)) + .withMessageContaining("does-not-exist.properties"); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProviderTest.java similarity index 61% rename from operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProviderTest.java rename to operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProviderTest.java index 06a3c8489c..2399524074 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/DefaultConfigProviderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProviderTest.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.javaoperatorsdk.operator.config.loader; +package io.javaoperatorsdk.operator.config.loader.provider; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; @@ -23,61 +23,25 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -class DefaultConfigProviderTest { - - private final DefaultConfigProvider provider = new DefaultConfigProvider(); +class SystemPropertyConfigProviderTest { @Test - void returnsEmptyWhenNeitherEnvNorPropertyIsSet() { + void returnsEmptyWhenPropertyAbsent() { + var provider = new SystemPropertyConfigProvider(k -> null); assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty(); } - // -- env variable tests ----------------------------------------------------- - - @Test - void readsStringFromEnvVariable() { - var envProvider = - new DefaultConfigProvider(k -> k.equals("JOSDK_TEST_STRING") ? "from-env" : null); - assertThat(envProvider.getValue("josdk.test.string", String.class)).hasValue("from-env"); - } - @Test - void envVariableKeyUsesUppercaseWithUnderscores() { - // dots and hyphens both become underscores, key is uppercased - var envProvider = - new DefaultConfigProvider(k -> k.equals("JOSDK_CACHE_SYNC_TIMEOUT") ? "PT10S" : null); - assertThat(envProvider.getValue("josdk.cache-sync.timeout", Duration.class)) - .hasValue(Duration.ofSeconds(10)); - } - - @Test - void envVariableTakesPrecedenceOverSystemProperty() { - System.setProperty("josdk.test.precedence", "from-sysprop"); - try { - var envProvider = - new DefaultConfigProvider(k -> k.equals("JOSDK_TEST_PRECEDENCE") ? "from-env" : null); - assertThat(envProvider.getValue("josdk.test.precedence", String.class)).hasValue("from-env"); - } finally { - System.clearProperty("josdk.test.precedence"); - } - } - - @Test - void fallsBackToSystemPropertyWhenEnvVariableAbsent() { - System.setProperty("josdk.test.fallback", "from-sysprop"); - try { - var envProvider = new DefaultConfigProvider(k -> null); - assertThat(envProvider.getValue("josdk.test.fallback", String.class)) - .hasValue("from-sysprop"); - } finally { - System.clearProperty("josdk.test.fallback"); - } + void returnsEmptyForNullKey() { + var provider = new SystemPropertyConfigProvider(k -> "value"); + assertThat(provider.getValue(null, String.class)).isEmpty(); } @Test void readsStringFromSystemProperty() { System.setProperty("josdk.test.string", "hello"); try { + var provider = new SystemPropertyConfigProvider(); assertThat(provider.getValue("josdk.test.string", String.class)).hasValue("hello"); } finally { System.clearProperty("josdk.test.string"); @@ -88,6 +52,7 @@ void readsStringFromSystemProperty() { void readsBooleanFromSystemProperty() { System.setProperty("josdk.test.bool", "true"); try { + var provider = new SystemPropertyConfigProvider(); assertThat(provider.getValue("josdk.test.bool", Boolean.class)).hasValue(true); } finally { System.clearProperty("josdk.test.bool"); @@ -98,6 +63,7 @@ void readsBooleanFromSystemProperty() { void readsIntegerFromSystemProperty() { System.setProperty("josdk.test.integer", "42"); try { + var provider = new SystemPropertyConfigProvider(); assertThat(provider.getValue("josdk.test.integer", Integer.class)).hasValue(42); } finally { System.clearProperty("josdk.test.integer"); @@ -108,6 +74,7 @@ void readsIntegerFromSystemProperty() { void readsLongFromSystemProperty() { System.setProperty("josdk.test.long", "123456789"); try { + var provider = new SystemPropertyConfigProvider(); assertThat(provider.getValue("josdk.test.long", Long.class)).hasValue(123456789L); } finally { System.clearProperty("josdk.test.long"); @@ -118,6 +85,7 @@ void readsLongFromSystemProperty() { void readsDurationFromSystemProperty() { System.setProperty("josdk.test.duration", "PT30S"); try { + var provider = new SystemPropertyConfigProvider(); assertThat(provider.getValue("josdk.test.duration", Duration.class)) .hasValue(Duration.ofSeconds(30)); } finally { @@ -129,6 +97,7 @@ void readsDurationFromSystemProperty() { void throwsForUnsupportedType() { System.setProperty("josdk.test.unsupported", "value"); try { + var provider = new SystemPropertyConfigProvider(); assertThatIllegalArgumentException() .isThrownBy(() -> provider.getValue("josdk.test.unsupported", AtomicInteger.class)) .withMessageContaining("Unsupported config type"); From 069772fba366a04d8e72b563891392aea46d145f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 24 Feb 2026 18:32:20 +0100 Subject: [PATCH 18/28] sample with smallrye config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/config/loader/ConfigLoader.java | 13 +++- sample-operators/tomcat-operator/pom.xml | 23 +++++++ .../operator/sample/TomcatOperator.java | 15 ++++- .../SmallryeConfigProvider.java | 35 +++++++++++ .../src/main/resources/application.yaml | 62 +++++++++++++++++++ 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/smallryeconfig/SmallryeConfigProvider.java create mode 100644 sample-operators/tomcat-operator/src/main/resources/application.yaml diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java index 9d04c3154e..9eea2a4721 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java @@ -20,6 +20,9 @@ import java.util.Optional; import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; @@ -30,6 +33,8 @@ public class ConfigLoader { + private static final Logger log = LoggerFactory.getLogger(ConfigLoader.class); + private static final ConfigLoader DEFAULT = new ConfigLoader(); public static ConfigLoader getDefault() { @@ -251,7 +256,13 @@ private Consumer buildConsumer(List> bindings, String private Consumer resolveStep(ConfigBinding binding, String key) { return configProvider .getValue(key, binding.type()) - .map(value -> (Consumer) overrider -> binding.setter().accept(overrider, value)) + .map( + value -> + (Consumer) + overrider -> { + log.debug("Found config property: {} = {}", key, value); + binding.setter().accept(overrider, value); + }) .orElse(null); } } diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index b7c3b05c98..c9afd825de 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -39,6 +39,13 @@ pom import + + io.smallrye.config + smallrye-config-bom + 3.16.0 + pom + import + @@ -92,6 +99,22 @@ operator-framework-junit test + + + io.smallrye.config + smallrye-config + 3.11.4 + + + io.smallrye.config + smallrye-config-source-yaml + 3.11.4 + + + org.eclipse.microprofile.config + microprofile-config-api + 3.1 + diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java index bb37892c11..7cc5d9027e 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -16,6 +16,7 @@ package io.javaoperatorsdk.operator.sample; import java.io.IOException; +import java.net.URL; import org.takes.facets.fork.FkRegex; import org.takes.facets.fork.TkFork; @@ -24,11 +25,23 @@ import io.javaoperatorsdk.operator.Operator; import io.javaoperatorsdk.operator.config.loader.ConfigLoader; +import io.javaoperatorsdk.operator.sample.smallryeconfig.SmallryeConfigProvider; +import io.smallrye.config.SmallRyeConfigBuilder; +import io.smallrye.config.source.yaml.YamlConfigSource; public class TomcatOperator { public static void main(String[] args) throws IOException { - var configLoader = ConfigLoader.getDefault(); + + URL configUrl = TomcatOperator.class.getResource("/application.yaml"); + if (configUrl == null) { + throw new IllegalStateException("application.yaml not found on classpath"); + } + var configLoader = + new ConfigLoader( + new SmallryeConfigProvider( + new SmallRyeConfigBuilder().withSources(new YamlConfigSource(configUrl)).build())); + Operator operator = new Operator(configLoader.applyConfigs()); operator.register( new TomcatReconciler(), configLoader.applyControllerConfigs(TomcatReconciler.NAME)); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/smallryeconfig/SmallryeConfigProvider.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/smallryeconfig/SmallryeConfigProvider.java new file mode 100644 index 0000000000..b415566af2 --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/smallryeconfig/SmallryeConfigProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.sample.smallryeconfig; + +import java.util.Optional; + +import io.javaoperatorsdk.operator.config.loader.ConfigProvider; +import io.smallrye.config.SmallRyeConfig; + +public class SmallryeConfigProvider implements ConfigProvider { + + private final SmallRyeConfig smallRyeConfig; + + public SmallryeConfigProvider(SmallRyeConfig smallRyeConfig) { + this.smallRyeConfig = smallRyeConfig; + } + + @Override + public Optional getValue(String key, Class type) { + return smallRyeConfig.getOptionalValue(key, type); + } +} diff --git a/sample-operators/tomcat-operator/src/main/resources/application.yaml b/sample-operators/tomcat-operator/src/main/resources/application.yaml new file mode 100644 index 0000000000..128ee5eabc --- /dev/null +++ b/sample-operators/tomcat-operator/src/main/resources/application.yaml @@ -0,0 +1,62 @@ +# +# Copyright Java Operator SDK Authors +# +# Licensed 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. +# + +# JOSDK operator-level configuration (josdk.) +josdk: + check-crd: true + close-client-on-stop: true + use-ssa-to-patch-primary-resource: false + clone-secondary-resources-when-getting-from-cache: false + reconciliation: + termination-timeout: PT30S + concurrent-threads: 10 + workflow: + executor-threads: 10 + informer: + stop-on-error-during-startup: true + cache-sync-timeout: PT2M + dependent-resources: + ssa-based-create-update-match: true + + # Controller-level configuration (josdk.controller..) + controller: + tomcat: + finalizer: tomcat.sample.javaoperatorsdk.io/finalizer + generation-aware: true + label-selector: "" + max-reconciliation-interval: PT10M + field-manager: tomcat-controller + trigger-reconciler-on-all-events: false + informer-list-limit: 500 + retry: + max-attempts: 10 + initial-interval: 2000 + interval-multiplier: 1.5 + max-interval: 60000 + + webapp: + finalizer: webapp.sample.javaoperatorsdk.io/finalizer + generation-aware: true + label-selector: "" + max-reconciliation-interval: PT10M + field-manager: webapp-controller + trigger-reconciler-on-all-events: false + informer-list-limit: 500 + retry: + max-attempts: 10 + initial-interval: 2000 + interval-multiplier: 1.5 + max-interval: 60000 From 81d3e49fac8d3c64b7613b1f57e06353e6141696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 25 Feb 2026 12:32:44 +0100 Subject: [PATCH 19/28] yaml config provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/config/loader/ConfigLoader.java | 4 +- ...=> AgregatePrirityListConfigProvider.java} | 4 +- .../loader/provider/YamlConfigProvider.java | 89 +++++++++++++++++++ .../config/loader/ConfigLoaderTest.java | 4 +- .../PriorityListConfigProviderTest.java | 8 +- 5 files changed, 99 insertions(+), 10 deletions(-) rename operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/{PrirityListConfigProvider.java => AgregatePrirityListConfigProvider.java} (89%) create mode 100644 operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java index 9eea2a4721..a15b4509d4 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java @@ -26,8 +26,8 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; +import io.javaoperatorsdk.operator.config.loader.provider.AgregatePrirityListConfigProvider; import io.javaoperatorsdk.operator.config.loader.provider.EnvVarConfigProvider; -import io.javaoperatorsdk.operator.config.loader.provider.PrirityListConfigProvider; import io.javaoperatorsdk.operator.config.loader.provider.SystemPropertyConfigProvider; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; @@ -143,7 +143,7 @@ public static ConfigLoader getDefault() { public ConfigLoader() { this( - new PrirityListConfigProvider( + new AgregatePrirityListConfigProvider( List.of(new EnvVarConfigProvider(), new SystemPropertyConfigProvider())), DEFAULT_CONTROLLER_KEY_PREFIX, DEFAULT_OPERATOR_KEY_PREFIX); diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PrirityListConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/AgregatePrirityListConfigProvider.java similarity index 89% rename from operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PrirityListConfigProvider.java rename to operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/AgregatePrirityListConfigProvider.java index 8d43f2e0ce..8da7d28f2f 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PrirityListConfigProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/AgregatePrirityListConfigProvider.java @@ -24,11 +24,11 @@ * A {@link ConfigProvider} that delegates to an ordered list of providers. Providers are queried in * list order; the first non-empty result wins. */ -public class PrirityListConfigProvider implements ConfigProvider { +public class AgregatePrirityListConfigProvider implements ConfigProvider { private final List providers; - public PrirityListConfigProvider(List providers) { + public AgregatePrirityListConfigProvider(List providers) { this.providers = List.copyOf(providers); } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java new file mode 100644 index 0000000000..52b07b011d --- /dev/null +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProvider.java @@ -0,0 +1,89 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Optional; + +import io.javaoperatorsdk.operator.config.loader.ConfigProvider; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +/** + * A {@link ConfigProvider} that resolves configuration values from a YAML file. + * + *

Keys use dot-separated notation to address nested YAML mappings (e.g. {@code + * josdk.cache-sync.timeout} maps to {@code josdk → cache-sync → timeout} in the YAML document). + * Leaf values are converted to the requested type via {@link ConfigValueConverter}. Supported value + * types are: {@link String}, {@link Boolean}, {@link Integer}, {@link Long}, {@link Double}, and + * {@link java.time.Duration} (ISO-8601 format, e.g. {@code PT30S}). + */ +public class YamlConfigProvider implements ConfigProvider { + + private static final ObjectMapper MAPPER = new ObjectMapper(new YAMLFactory()); + + private final Map data; + + /** + * Loads YAML from the given file path. + * + * @throws UncheckedIOException if the file cannot be read + */ + public YamlConfigProvider(Path path) { + this.data = load(path); + } + + /** Uses the supplied map directly (useful for testing). */ + public YamlConfigProvider(Map data) { + this.data = data; + } + + @Override + @SuppressWarnings("unchecked") + public Optional getValue(String key, Class type) { + if (key == null) { + return Optional.empty(); + } + String[] parts = key.split("\\.", -1); + Object current = data; + for (String part : parts) { + if (!(current instanceof Map)) { + return Optional.empty(); + } + current = ((Map) current).get(part); + if (current == null) { + return Optional.empty(); + } + } + return Optional.of(ConfigValueConverter.convert(current.toString(), type)); + } + + @SuppressWarnings("unchecked") + private static Map load(Path path) { + try (InputStream in = Files.newInputStream(path)) { + Map result = MAPPER.readValue(in, Map.class); + return result != null ? result : Map.of(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load config YAML from " + path, e); + } + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java index 460eebcbc9..e59d943f17 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java @@ -216,8 +216,8 @@ void controllerKeyPrefixIsJosdkControllerDot() { // -- binding coverage ------------------------------------------------------- /** - * Supported scalar types that PrirityListConfigProvider can parse from a string. Every binding's - * type must be one of these. + * Supported scalar types that AgregatePrirityListConfigProvider can parse from a string. Every + * binding's type must be one of these. */ private static final Set> SUPPORTED_TYPES = Set.of( diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java index b5fa8d3a24..85ec2f2d28 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java @@ -26,7 +26,7 @@ class PriorityListConfigProviderTest { @Test void returnsEmptyWhenAllProvidersReturnEmpty() { var provider = - new PrirityListConfigProvider( + new AgregatePrirityListConfigProvider( List.of( new EnvVarConfigProvider(k -> null), new SystemPropertyConfigProvider(k -> null))); assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty(); @@ -35,7 +35,7 @@ void returnsEmptyWhenAllProvidersReturnEmpty() { @Test void firstProviderWins() { var provider = - new PrirityListConfigProvider( + new AgregatePrirityListConfigProvider( List.of( new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_KEY") ? "first" : null), new SystemPropertyConfigProvider( @@ -46,7 +46,7 @@ void firstProviderWins() { @Test void fallsBackToLaterProviderWhenEarlierReturnsEmpty() { var provider = - new PrirityListConfigProvider( + new AgregatePrirityListConfigProvider( List.of( new EnvVarConfigProvider(k -> null), new SystemPropertyConfigProvider( @@ -61,7 +61,7 @@ void respectsOrderWithThreeProviders() { new SystemPropertyConfigProvider(k -> k.equals("josdk.test.key") ? "from-second" : null); var third = new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_KEY") ? "from-third" : null); - var provider = new PrirityListConfigProvider(List.of(first, second, third)); + var provider = new AgregatePrirityListConfigProvider(List.of(first, second, third)); assertThat(provider.getValue("josdk.test.key", String.class)).hasValue("from-second"); } } From 527013ba70d3f87e542228aa69eb5c60fda76872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 25 Feb 2026 14:51:26 +0100 Subject: [PATCH 20/28] add leader election configs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/config/loader/ConfigLoader.java | 76 +++++++++++++++++- .../config/loader/ConfigLoaderTest.java | 79 +++++++++++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java index a15b4509d4..79e0303066 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java @@ -26,6 +26,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; +import io.javaoperatorsdk.operator.api.config.LeaderElectionConfigurationBuilder; import io.javaoperatorsdk.operator.config.loader.provider.AgregatePrirityListConfigProvider; import io.javaoperatorsdk.operator.config.loader.provider.EnvVarConfigProvider; import io.javaoperatorsdk.operator.config.loader.provider.SystemPropertyConfigProvider; @@ -101,6 +102,17 @@ public static ConfigLoader getDefault() { Boolean.class, ConfigurationServiceOverrider::withCloneSecondaryResourcesWhenGettingFromCache)); + // --------------------------------------------------------------------------- + // Operator-level leader-election property keys + // --------------------------------------------------------------------------- + static final String LEADER_ELECTION_ENABLED_KEY = "leader-election.enabled"; + static final String LEADER_ELECTION_LEASE_NAME_KEY = "leader-election.lease-name"; + static final String LEADER_ELECTION_LEASE_NAMESPACE_KEY = "leader-election.lease-namespace"; + static final String LEADER_ELECTION_IDENTITY_KEY = "leader-election.identity"; + static final String LEADER_ELECTION_LEASE_DURATION_KEY = "leader-election.lease-duration"; + static final String LEADER_ELECTION_RENEW_DEADLINE_KEY = "leader-election.renew-deadline"; + static final String LEADER_ELECTION_RETRY_PERIOD_KEY = "leader-election.retry-period"; + // --------------------------------------------------------------------------- // Controller-level retry property suffixes // --------------------------------------------------------------------------- @@ -166,7 +178,15 @@ public ConfigLoader( * no binding has a matching value, preserving the previous behavior. */ public Consumer applyConfigs() { - return buildConsumer(OPERATOR_BINDINGS, operatorKeyPrefix); + Consumer consumer = + buildConsumer(OPERATOR_BINDINGS, operatorKeyPrefix); + + Consumer leaderElectionStep = + buildLeaderElectionConsumer(operatorKeyPrefix); + if (leaderElectionStep != null) { + consumer = consumer.andThen(leaderElectionStep); + } + return consumer; } /** @@ -226,6 +246,60 @@ private Consumer> bu }; } + /** + * If leader election is explicitly disabled via {@code leader-election.enabled=false}, returns + * {@code null}. Otherwise, if at least one leader-election property is present (with {@code + * leader-election.lease-name} being required), returns a {@link Consumer} that builds a {@link + * io.javaoperatorsdk.operator.api.config.LeaderElectionConfiguration} via {@link + * LeaderElectionConfigurationBuilder} and applies it to the overrider. Returns {@code null} when + * no leader-election properties are present at all. + */ + private Consumer buildLeaderElectionConsumer(String prefix) { + Optional enabled = + configProvider.getValue(prefix + LEADER_ELECTION_ENABLED_KEY, Boolean.class); + if (enabled.isPresent() && !enabled.get()) { + return null; + } + + Optional leaseName = + configProvider.getValue(prefix + LEADER_ELECTION_LEASE_NAME_KEY, String.class); + Optional leaseNamespace = + configProvider.getValue(prefix + LEADER_ELECTION_LEASE_NAMESPACE_KEY, String.class); + Optional identity = + configProvider.getValue(prefix + LEADER_ELECTION_IDENTITY_KEY, String.class); + Optional leaseDuration = + configProvider.getValue(prefix + LEADER_ELECTION_LEASE_DURATION_KEY, Duration.class); + Optional renewDeadline = + configProvider.getValue(prefix + LEADER_ELECTION_RENEW_DEADLINE_KEY, Duration.class); + Optional retryPeriod = + configProvider.getValue(prefix + LEADER_ELECTION_RETRY_PERIOD_KEY, Duration.class); + + if (leaseName.isEmpty() + && leaseNamespace.isEmpty() + && identity.isEmpty() + && leaseDuration.isEmpty() + && renewDeadline.isEmpty() + && retryPeriod.isEmpty()) { + return null; + } + + return overrider -> { + var builder = + LeaderElectionConfigurationBuilder.aLeaderElectionConfiguration( + leaseName.orElseThrow( + () -> + new IllegalStateException( + "leader-election.lease-name must be set when configuring leader" + + " election"))); + leaseNamespace.ifPresent(builder::withLeaseNamespace); + identity.ifPresent(builder::withIdentity); + leaseDuration.ifPresent(builder::withLeaseDuration); + renewDeadline.ifPresent(builder::withRenewDeadline); + retryPeriod.ifPresent(builder::withRetryPeriod); + overrider.withLeaderElectionConfiguration(builder.build()); + }; + } + /** * Iterates {@code bindings} and, for each one whose key (optionally prefixed by {@code * keyPrefix}) is present in the {@link ConfigProvider}, accumulates a call to the binding's diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java index e59d943f17..70e18ddb57 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java @@ -32,6 +32,7 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; class ConfigLoaderTest { @@ -288,6 +289,84 @@ void controllerBindingsCoverAllSingleScalarSettersOnControllerConfigurationOverr .containsExactlyInAnyOrderElementsOf(expectedSetters); } + // -- leader election -------------------------------------------------------- + + @Test + void leaderElectionIsNotConfiguredWhenNoPropertiesPresent() { + var loader = new ConfigLoader(mapProvider(Map.of())); + var base = new BaseConfigurationService(null); + var result = + ConfigurationService.newOverriddenConfigurationService(base, loader.applyConfigs()); + assertThat(result.getLeaderElectionConfiguration()).isEmpty(); + } + + @Test + void leaderElectionIsNotConfiguredWhenExplicitlyDisabled() { + var values = new HashMap(); + values.put("josdk.leader-election.enabled", false); + values.put("josdk.leader-election.lease-name", "my-lease"); + var loader = new ConfigLoader(mapProvider(values)); + var base = new BaseConfigurationService(null); + var result = + ConfigurationService.newOverriddenConfigurationService(base, loader.applyConfigs()); + assertThat(result.getLeaderElectionConfiguration()).isEmpty(); + } + + @Test + void leaderElectionConfiguredWithLeaseNameOnly() { + var loader = + new ConfigLoader(mapProvider(Map.of("josdk.leader-election.lease-name", "my-lease"))); + var base = new BaseConfigurationService(null); + var result = + ConfigurationService.newOverriddenConfigurationService(base, loader.applyConfigs()); + assertThat(result.getLeaderElectionConfiguration()) + .hasValueSatisfying( + le -> { + assertThat(le.getLeaseName()).isEqualTo("my-lease"); + assertThat(le.getLeaseNamespace()).isEmpty(); + assertThat(le.getIdentity()).isEmpty(); + }); + } + + @Test + void leaderElectionConfiguredWithAllProperties() { + var values = new HashMap(); + values.put("josdk.leader-election.enabled", true); + values.put("josdk.leader-election.lease-name", "my-lease"); + values.put("josdk.leader-election.lease-namespace", "my-ns"); + values.put("josdk.leader-election.identity", "pod-1"); + values.put("josdk.leader-election.lease-duration", Duration.ofSeconds(20)); + values.put("josdk.leader-election.renew-deadline", Duration.ofSeconds(15)); + values.put("josdk.leader-election.retry-period", Duration.ofSeconds(3)); + var loader = new ConfigLoader(mapProvider(values)); + + var base = new BaseConfigurationService(null); + var result = + ConfigurationService.newOverriddenConfigurationService(base, loader.applyConfigs()); + + assertThat(result.getLeaderElectionConfiguration()) + .hasValueSatisfying( + le -> { + assertThat(le.getLeaseName()).isEqualTo("my-lease"); + assertThat(le.getLeaseNamespace()).hasValue("my-ns"); + assertThat(le.getIdentity()).hasValue("pod-1"); + assertThat(le.getLeaseDuration()).isEqualTo(Duration.ofSeconds(20)); + assertThat(le.getRenewDeadline()).isEqualTo(Duration.ofSeconds(15)); + assertThat(le.getRetryPeriod()).isEqualTo(Duration.ofSeconds(3)); + }); + } + + @Test + void leaderElectionMissingLeaseNameThrowsWhenOtherPropertiesPresent() { + var loader = + new ConfigLoader(mapProvider(Map.of("josdk.leader-election.lease-namespace", "my-ns"))); + var base = new BaseConfigurationService(null); + var consumer = loader.applyConfigs(); + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy(() -> ConfigurationService.newOverriddenConfigurationService(base, consumer)) + .withMessageContaining("lease-name"); + } + /** Returns true when the two types are the same or one is the boxed/unboxed form of the other. */ private static boolean isTypeCompatible(Class methodParam, Class bindingType) { if (methodParam == bindingType) return true; From 35f22ca98e0868b11e5188e9c49fd00e1eb77fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Wed, 25 Feb 2026 17:11:47 +0100 Subject: [PATCH 21/28] rate limiter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/config/loader/ConfigLoader.java | 44 ++++++++++++++++++- .../config/loader/ConfigLoaderTest.java | 25 ++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java index 79e0303066..d93449a32f 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java @@ -30,6 +30,7 @@ import io.javaoperatorsdk.operator.config.loader.provider.AgregatePrirityListConfigProvider; import io.javaoperatorsdk.operator.config.loader.provider.EnvVarConfigProvider; import io.javaoperatorsdk.operator.config.loader.provider.SystemPropertyConfigProvider; +import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; public class ConfigLoader { @@ -121,6 +122,12 @@ public static ConfigLoader getDefault() { static final String RETRY_INTERVAL_MULTIPLIER_SUFFIX = "retry.interval-multiplier"; static final String RETRY_MAX_INTERVAL_SUFFIX = "retry.max-interval"; + // --------------------------------------------------------------------------- + // Controller-level rate-limiter property suffixes + // --------------------------------------------------------------------------- + static final String RATE_LIMITER_REFRESH_PERIOD_SUFFIX = "rate-limiter.refresh-period"; + static final String RATE_LIMITER_LIMIT_FOR_PERIOD_SUFFIX = "rate-limiter.limit-for-period"; + // --------------------------------------------------------------------------- // Controller-level (ControllerConfigurationOverrider) bindings // The key used at runtime is built as: @@ -147,7 +154,11 @@ public static ConfigLoader getDefault() { Boolean.class, ControllerConfigurationOverrider::withTriggerReconcilerOnAllEvents), new ConfigBinding<>( - "informer-list-limit", + "informer.label-selector", + String.class, + ControllerConfigurationOverrider::withLabelSelector), + new ConfigBinding<>( + "informer.list-limit", Long.class, ControllerConfigurationOverrider::withInformerListLimit)); @@ -210,6 +221,11 @@ Consumer> applyControllerConfigs(String cont if (retryStep != null) { consumer = consumer == null ? retryStep : consumer.andThen(retryStep); } + Consumer> rateLimiterStep = + buildRateLimiterConsumer(prefix); + if (rateLimiterStep != null) { + consumer = consumer.andThen(rateLimiterStep); + } return consumer; } @@ -246,6 +262,32 @@ private Consumer> bu }; } + /** + * Returns a {@link Consumer} that builds a {@link LinearRateLimiter} only if {@code + * rate-limiter.limit-for-period} is present and positive (a non-positive value would deactivate + * the limiter and is therefore treated as absent). {@code rate-limiter.refresh-period} is applied + * when also present; otherwise the default refresh period is used. Returns {@code null} when no + * effective rate-limiter configuration is found. + */ + private + Consumer> buildRateLimiterConsumer(String prefix) { + Optional refreshPeriod = + configProvider.getValue(prefix + RATE_LIMITER_REFRESH_PERIOD_SUFFIX, Duration.class); + Optional limitForPeriod = + configProvider.getValue(prefix + RATE_LIMITER_LIMIT_FOR_PERIOD_SUFFIX, Integer.class); + + if (limitForPeriod.isEmpty() || limitForPeriod.get() <= 0) { + return null; + } + + return overrider -> { + var rateLimiter = + new LinearRateLimiter( + refreshPeriod.orElse(LinearRateLimiter.DEFAULT_REFRESH_PERIOD), limitForPeriod.get()); + overrider.withRateLimiter(rateLimiter); + }; + } + /** * If leader election is explicitly disabled via {@code leader-election.enabled=false}, returns * {@code null}. Otherwise, if at least one leader-election property is present (with {@code diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java index 70e18ddb57..4fe9d50c2a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java @@ -201,7 +201,10 @@ public Optional getValue(String key, Class type) { "josdk.controller.ctrl.max-reconciliation-interval", "josdk.controller.ctrl.field-manager", "josdk.controller.ctrl.trigger-reconciler-on-all-events", - "josdk.controller.ctrl.informer-list-limit"); + "josdk.controller.ctrl.informer.label-selector", + "josdk.controller.ctrl.informer.list-limit", + "josdk.controller.ctrl.rate-limiter.refresh-period", + "josdk.controller.ctrl.rate-limiter.limit-for-period"); } @Test @@ -214,6 +217,26 @@ void controllerKeyPrefixIsJosdkControllerDot() { assertThat(ConfigLoader.DEFAULT_CONTROLLER_KEY_PREFIX).isEqualTo("josdk.controller."); } + // -- rate limiter ----------------------------------------------------------- + + @Test + void rateLimiterQueriesExpectedKeys() { + var queriedKeys = new ArrayList(); + ConfigProvider recordingProvider = + new ConfigProvider() { + @Override + public Optional getValue(String key, Class type) { + queriedKeys.add(key); + return Optional.empty(); + } + }; + new ConfigLoader(recordingProvider).applyControllerConfigs("ctrl"); + assertThat(queriedKeys) + .contains( + "josdk.controller.ctrl.rate-limiter.refresh-period", + "josdk.controller.ctrl.rate-limiter.limit-for-period"); + } + // -- binding coverage ------------------------------------------------------- /** From a6d4e4eae159ca8bffb7286f3ec4c020805a7812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 26 Feb 2026 13:04:49 +0100 Subject: [PATCH 22/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../provider/YamlConfigProviderTest.java | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java new file mode 100644 index 0000000000..4f8c53ac38 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/YamlConfigProviderTest.java @@ -0,0 +1,144 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class YamlConfigProviderTest { + + // -- Map constructor -------------------------------------------------------- + + @Test + void returnsEmptyWhenKeyAbsent() { + var provider = new YamlConfigProvider(Map.of()); + assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty(); + } + + @Test + void returnsEmptyForNullKey() { + var provider = new YamlConfigProvider(Map.of("josdk", Map.of("test", "value"))); + assertThat(provider.getValue(null, String.class)).isEmpty(); + } + + @Test + void readsTopLevelString() { + var provider = new YamlConfigProvider(Map.of("key", "hello")); + assertThat(provider.getValue("key", String.class)).hasValue("hello"); + } + + @Test + void readsNestedString() { + var provider = + new YamlConfigProvider(Map.of("josdk", Map.of("test", Map.of("string", "hello")))); + assertThat(provider.getValue("josdk.test.string", String.class)).hasValue("hello"); + } + + @Test + void readsBoolean() { + var provider = new YamlConfigProvider(Map.of("josdk", Map.of("test", Map.of("bool", "true")))); + assertThat(provider.getValue("josdk.test.bool", Boolean.class)).hasValue(true); + } + + @Test + void readsInteger() { + var provider = new YamlConfigProvider(Map.of("josdk", Map.of("test", Map.of("integer", 42)))); + assertThat(provider.getValue("josdk.test.integer", Integer.class)).hasValue(42); + } + + @Test + void readsLong() { + var provider = + new YamlConfigProvider(Map.of("josdk", Map.of("test", Map.of("long", 123456789L)))); + assertThat(provider.getValue("josdk.test.long", Long.class)).hasValue(123456789L); + } + + @Test + void readsDouble() { + var provider = + new YamlConfigProvider(Map.of("josdk", Map.of("test", Map.of("double", "3.14")))); + assertThat(provider.getValue("josdk.test.double", Double.class)).hasValue(3.14); + } + + @Test + void readsDuration() { + var provider = + new YamlConfigProvider(Map.of("josdk", Map.of("test", Map.of("duration", "PT30S")))); + assertThat(provider.getValue("josdk.test.duration", Duration.class)) + .hasValue(Duration.ofSeconds(30)); + } + + @Test + void returnsEmptyWhenIntermediateSegmentMissing() { + var provider = new YamlConfigProvider(Map.of("josdk", Map.of("other", "value"))); + assertThat(provider.getValue("josdk.test.key", String.class)).isEmpty(); + } + + @Test + void returnsEmptyWhenIntermediateSegmentIsLeaf() { + // "josdk.test" is a leaf – trying to drill further should return empty + var provider = new YamlConfigProvider(Map.of("josdk", Map.of("test", "leaf"))); + assertThat(provider.getValue("josdk.test.key", String.class)).isEmpty(); + } + + @Test + void throwsForUnsupportedType() { + var provider = + new YamlConfigProvider(Map.of("josdk", Map.of("test", Map.of("unsupported", "value")))); + assertThatIllegalArgumentException() + .isThrownBy(() -> provider.getValue("josdk.test.unsupported", AtomicInteger.class)) + .withMessageContaining("Unsupported config type"); + } + + // -- Path constructor ------------------------------------------------------- + + @Test + void loadsFromFile(@TempDir Path dir) throws IOException { + Path file = dir.resolve("test.yaml"); + Files.writeString( + file, + """ + josdk: + test: + string: from-file + integer: 7 + """); + + var provider = new YamlConfigProvider(file); + assertThat(provider.getValue("josdk.test.string", String.class)).hasValue("from-file"); + assertThat(provider.getValue("josdk.test.integer", Integer.class)).hasValue(7); + } + + @Test + void throwsUncheckedIOExceptionForMissingFile(@TempDir Path dir) { + Path missing = dir.resolve("does-not-exist.yaml"); + assertThatExceptionOfType(UncheckedIOException.class) + .isThrownBy(() -> new YamlConfigProvider(missing)) + .withMessageContaining("does-not-exist.yaml"); + } +} From dc6a277b55871ab37d5c916dfa01f6c2e1ff85b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 26 Feb 2026 17:40:53 +0100 Subject: [PATCH 23/28] Cleanup and documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../en/docs/documentation/configuration.md | 210 +++++++++++++++++- .../operator/config/loader/ConfigLoader.java | 4 +- .../provider/PropertiesConfigProvider.java | 5 + .../SystemPropertyConfigProvider.java | 53 ----- .../PriorityListConfigProviderTest.java | 21 +- .../SystemPropertyConfigProviderTest.java | 108 --------- 6 files changed, 228 insertions(+), 173 deletions(-) delete mode 100644 operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProvider.java delete mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProviderTest.java diff --git a/docs/content/en/docs/documentation/configuration.md b/docs/content/en/docs/documentation/configuration.md index 888804628f..e761f7e1f5 100644 --- a/docs/content/en/docs/documentation/configuration.md +++ b/docs/content/en/docs/documentation/configuration.md @@ -149,6 +149,212 @@ For more information on how to use this feature, we recommend looking at how thi `KubernetesDependentResource` in the core framework, `SchemaDependentResource` in the samples or `CustomAnnotationDep` in the `BaseConfigurationServiceTest` test class. -## EventSource-level configuration +## Loading Configuration from External Sources + +JOSDK ships a `ConfigLoader` that bridges any key-value configuration source to the operator and +controller configuration APIs. This lets you drive operator behaviour from environment variables, +system properties, YAML files, or any config library (MicroProfile Config, SmallRye Config, +Spring Environment, etc.) without writing glue code by hand. + +### Architecture + +The system is built around two thin abstractions: + +- **[`ConfigProvider`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigProvider.java)** + — a single-method interface that resolves a typed value for a dot-separated key: + + ```java + public interface ConfigProvider { + Optional getValue(String key, Class type); + } + ``` + +- **[`ConfigLoader`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java)** + — reads all known JOSDK keys from a `ConfigProvider` and returns + `Consumer` / `Consumer>` + values that you pass directly to the `Operator` constructor or `operator.register()`. + +The default `ConfigLoader` (no-arg constructor) stacks environment variables over system +properties: environment variables win, system properties are the fallback. + +```java +// uses env vars + system properties out of the box +Operator operator = new Operator(ConfigLoader.getDefault().applyConfigs()); +``` + +### Built-in Providers + +| Provider | Source | Key mapping | +|---|---|---| +| `EnvVarConfigProvider` | `System.getenv()` | dots and hyphens → underscores, upper-cased (`josdk.check-crd` → `JOSDK_CHECK_CRD`) | +| `PropertiesConfigProvider` | `java.util.Properties` or `.properties` file | key used as-is; use `PropertiesConfigProvider.systemProperties()` to read Java system properties | +| `YamlConfigProvider` | YAML file | dot-separated key traverses nested mappings | +| `AgregatePrirityListConfigProvider` | ordered list of providers | first non-empty result wins | + +All string-based providers convert values to the target type automatically. +Supported types: `String`, `Boolean`, `Integer`, `Long`, `Double`, `Duration` (ISO-8601, e.g. `PT30S`). + +### Plugging in Any Config Library + +`ConfigProvider` is a single-method interface, so adapting any config library takes only a few +lines. As an example, here is an adapter for +[SmallRye Config](https://smallrye.io/smallrye-config/): + +```java +public class SmallRyeConfigProvider implements ConfigProvider { + + private final SmallRyeConfig config; + + public SmallRyeConfigProvider(SmallRyeConfig config) { + this.config = config; + } + + @Override + public Optional getValue(String key, Class type) { + return config.getOptionalValue(key, type); + } +} +``` + +The same pattern applies to MicroProfile Config, Spring `Environment`, Apache Commons +Configuration, or any other library that can look up typed values by string key. + +### Wiring Everything Together + +Pass the `ConfigLoader` results when constructing the operator and registering reconcilers: + +```java +// Load operator-wide config from a YAML file via SmallRye Config +URL configUrl = MyOperator.class.getResource("/application.yaml"); +var configLoader = new ConfigLoader( + new SmallRyeConfigProvider( + new SmallRyeConfigBuilder() + .withSources(new YamlConfigSource(configUrl)) + .build())); + +// applyConfigs() → Consumer +Operator operator = new Operator(configLoader.applyConfigs()); + +// applyControllerConfigs(name) → Consumer> +operator.register(new MyReconciler(), + configLoader.applyControllerConfigs(MyReconciler.NAME)); +``` + +Only keys that are actually present in the source are applied; everything else retains its +programmatic or annotation-based default. + +You can also compose multiple sources with explicit priority using +`AgregatePrirityListConfigProvider`: + +```java +var configLoader = new ConfigLoader( + new AgregatePrirityListConfigProvider(List.of( + new EnvVarConfigProvider(), // highest priority + PropertiesConfigProvider.systemProperties(), + new YamlConfigProvider(Path.of("config/operator.yaml")) // lowest priority + ))); +``` + +### Operator-Level Configuration Keys + +All operator-level keys are prefixed with `josdk.`. + +#### General + +| Key | Type | Description | +|---|---|---| +| `josdk.check-crd` | `Boolean` | Validate CRDs against local model on startup | +| `josdk.close-client-on-stop` | `Boolean` | Close the Kubernetes client when the operator stops | +| `josdk.use-ssa-to-patch-primary-resource` | `Boolean` | Use Server-Side Apply to patch the primary resource | +| `josdk.clone-secondary-resources-when-getting-from-cache` | `Boolean` | Clone secondary resources on cache reads | + +#### Reconciliation + +| Key | Type | Description | +|---|---|---| +| `josdk.reconciliation.concurrent-threads` | `Integer` | Thread pool size for reconciliation | +| `josdk.reconciliation.termination-timeout` | `Duration` | How long to wait for in-flight reconciliations to finish on shutdown | + +#### Workflow + +| Key | Type | Description | +|---|---|---| +| `josdk.workflow.executor-threads` | `Integer` | Thread pool size for workflow execution | + +#### Informer + +| Key | Type | Description | +|---|---|---| +| `josdk.informer.cache-sync-timeout` | `Duration` | Timeout for the initial informer cache sync | +| `josdk.informer.stop-on-error-during-startup` | `Boolean` | Stop the operator if an informer fails to start | + +#### Dependent Resources + +| Key | Type | Description | +|---|---|---| +| `josdk.dependent-resources.ssa-based-create-update-match` | `Boolean` | Use SSA-based matching for dependent resource create/update | + +#### Leader Election + +Leader election is activated when at least one `josdk.leader-election.*` key is present. +`josdk.leader-election.lease-name` is required when any other leader-election key is set. +Setting `josdk.leader-election.enabled=false` suppresses leader election even if other keys are +present. + +| Key | Type | Description | +|---|---|---| +| `josdk.leader-election.enabled` | `Boolean` | Explicitly enable (`true`) or disable (`false`) leader election | +| `josdk.leader-election.lease-name` | `String` | **Required.** Name of the Kubernetes Lease object used for leader election | +| `josdk.leader-election.lease-namespace` | `String` | Namespace for the Lease object (defaults to the operator's namespace) | +| `josdk.leader-election.identity` | `String` | Unique identity for this instance; defaults to the pod name | +| `josdk.leader-election.lease-duration` | `Duration` | How long a lease is valid (default `PT15S`) | +| `josdk.leader-election.renew-deadline` | `Duration` | How long the leader tries to renew before giving up (default `PT10S`) | +| `josdk.leader-election.retry-period` | `Duration` | How often a candidate polls while waiting to become leader (default `PT2S`) | + +### Controller-Level Configuration Keys + +All controller-level keys are prefixed with `josdk.controller..`, where +`` is the value returned by the reconciler's name (typically set via +`@ControllerConfiguration(name = "...")`). + +#### General + +| Key | Type | Description | +|---|---|---| +| `josdk.controller..finalizer` | `String` | Finalizer string added to managed resources | +| `josdk.controller..generation-aware` | `Boolean` | Skip reconciliation when the resource generation has not changed | +| `josdk.controller..label-selector` | `String` | Label selector to filter watched resources | +| `josdk.controller..max-reconciliation-interval` | `Duration` | Maximum interval between reconciliations even without events | +| `josdk.controller..field-manager` | `String` | Field manager name used for SSA operations | +| `josdk.controller..trigger-reconciler-on-all-events` | `Boolean` | Trigger reconciliation on every event, not only meaningful changes | + +#### Informer + +| Key | Type | Description | +|---|---|---| +| `josdk.controller..informer.label-selector` | `String` | Label selector for the primary resource informer (alias for `label-selector`) | +| `josdk.controller..informer.list-limit` | `Long` | Page size for paginated informer list requests; omit for no pagination | + +#### Retry + +If any `retry.*` key is present, a `GenericRetry` is configured starting from the +[default limited exponential retry](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/retry/GenericRetry.java). +Only explicitly set keys override the defaults. + +| Key | Type | Description | +|---|---|---| +| `josdk.controller..retry.max-attempts` | `Integer` | Maximum number of retry attempts | +| `josdk.controller..retry.initial-interval` | `Long` (ms) | Initial backoff interval in milliseconds | +| `josdk.controller..retry.interval-multiplier` | `Double` | Exponential backoff multiplier | +| `josdk.controller..retry.max-interval` | `Long` (ms) | Maximum backoff interval in milliseconds | + +#### Rate Limiter + +The rate limiter is only activated when `rate-limiter.limit-for-period` is present and has a +positive value. `rate-limiter.refresh-period` is optional and falls back to the default of 10 s. + +| Key | Type | Description | +|---|---|---| +| `josdk.controller..rate-limiter.limit-for-period` | `Integer` | Maximum number of reconciliations allowed per refresh period. Must be positive to activate the limiter | +| `josdk.controller..rate-limiter.refresh-period` | `Duration` | Window over which the limit is counted (default `PT10S`) | -TODO diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java index d93449a32f..cc3a044112 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java @@ -29,7 +29,7 @@ import io.javaoperatorsdk.operator.api.config.LeaderElectionConfigurationBuilder; import io.javaoperatorsdk.operator.config.loader.provider.AgregatePrirityListConfigProvider; import io.javaoperatorsdk.operator.config.loader.provider.EnvVarConfigProvider; -import io.javaoperatorsdk.operator.config.loader.provider.SystemPropertyConfigProvider; +import io.javaoperatorsdk.operator.config.loader.provider.PropertiesConfigProvider; import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; import io.javaoperatorsdk.operator.processing.retry.GenericRetry; @@ -167,7 +167,7 @@ public static ConfigLoader getDefault() { public ConfigLoader() { this( new AgregatePrirityListConfigProvider( - List.of(new EnvVarConfigProvider(), new SystemPropertyConfigProvider())), + List.of(new EnvVarConfigProvider(), PropertiesConfigProvider.systemProperties())), DEFAULT_CONTROLLER_KEY_PREFIX, DEFAULT_OPERATOR_KEY_PREFIX); } diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java index 01ef5b4b03..35dd38f406 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java @@ -36,6 +36,11 @@ public class PropertiesConfigProvider implements ConfigProvider { private final Properties properties; + /** Returns a {@link PropertiesConfigProvider} backed by {@link System#getProperties()}. */ + public static PropertiesConfigProvider systemProperties() { + return new PropertiesConfigProvider(System.getProperties()); + } + /** * Loads properties from the given file path. * diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProvider.java deleted file mode 100644 index f777eb378f..0000000000 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProvider.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Java Operator SDK Authors - * - * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; - -import java.util.Optional; -import java.util.function.Function; - -import io.javaoperatorsdk.operator.config.loader.ConfigProvider; - -/** - * A {@link ConfigProvider} that resolves configuration values from Java system properties via - * {@link System#getProperty(String)}, using the key as-is. - * - *

Supported value types are: {@link String}, {@link Boolean}, {@link Integer}, {@link Long}, - * {@link Double}, and {@link java.time.Duration} (ISO-8601 format, e.g. {@code PT30S}). - */ -public class SystemPropertyConfigProvider implements ConfigProvider { - - private final Function propertyLookup; - - public SystemPropertyConfigProvider() { - this(System::getProperty); - } - - SystemPropertyConfigProvider(Function propertyLookup) { - this.propertyLookup = propertyLookup; - } - - @Override - public Optional getValue(String key, Class type) { - if (key == null) { - return Optional.empty(); - } - String raw = propertyLookup.apply(key); - if (raw == null) { - return Optional.empty(); - } - return Optional.of(ConfigValueConverter.convert(raw, type)); - } -} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java index 85ec2f2d28..c4678b9810 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java @@ -16,6 +16,7 @@ package io.javaoperatorsdk.operator.config.loader.provider; import java.util.List; +import java.util.Properties; import org.junit.jupiter.api.Test; @@ -23,12 +24,19 @@ class PriorityListConfigProviderTest { + private static PropertiesConfigProvider propsProvider(String key, String value) { + Properties props = new Properties(); + if (key != null) { + props.setProperty(key, value); + } + return new PropertiesConfigProvider(props); + } + @Test void returnsEmptyWhenAllProvidersReturnEmpty() { var provider = new AgregatePrirityListConfigProvider( - List.of( - new EnvVarConfigProvider(k -> null), new SystemPropertyConfigProvider(k -> null))); + List.of(new EnvVarConfigProvider(k -> null), propsProvider(null, null))); assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty(); } @@ -38,8 +46,7 @@ void firstProviderWins() { new AgregatePrirityListConfigProvider( List.of( new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_KEY") ? "first" : null), - new SystemPropertyConfigProvider( - k -> k.equals("josdk.test.key") ? "second" : null))); + propsProvider("josdk.test.key", "second"))); assertThat(provider.getValue("josdk.test.key", String.class)).hasValue("first"); } @@ -49,16 +56,14 @@ void fallsBackToLaterProviderWhenEarlierReturnsEmpty() { new AgregatePrirityListConfigProvider( List.of( new EnvVarConfigProvider(k -> null), - new SystemPropertyConfigProvider( - k -> k.equals("josdk.test.key") ? "from-second" : null))); + propsProvider("josdk.test.key", "from-second"))); assertThat(provider.getValue("josdk.test.key", String.class)).hasValue("from-second"); } @Test void respectsOrderWithThreeProviders() { var first = new EnvVarConfigProvider(k -> null); - var second = - new SystemPropertyConfigProvider(k -> k.equals("josdk.test.key") ? "from-second" : null); + var second = propsProvider("josdk.test.key", "from-second"); var third = new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_KEY") ? "from-third" : null); var provider = new AgregatePrirityListConfigProvider(List.of(first, second, third)); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProviderTest.java deleted file mode 100644 index 2399524074..0000000000 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProviderTest.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Java Operator SDK Authors - * - * Licensed 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 io.javaoperatorsdk.operator.config.loader.provider; - -import java.time.Duration; -import java.util.concurrent.atomic.AtomicInteger; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; - -class SystemPropertyConfigProviderTest { - - @Test - void returnsEmptyWhenPropertyAbsent() { - var provider = new SystemPropertyConfigProvider(k -> null); - assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty(); - } - - @Test - void returnsEmptyForNullKey() { - var provider = new SystemPropertyConfigProvider(k -> "value"); - assertThat(provider.getValue(null, String.class)).isEmpty(); - } - - @Test - void readsStringFromSystemProperty() { - System.setProperty("josdk.test.string", "hello"); - try { - var provider = new SystemPropertyConfigProvider(); - assertThat(provider.getValue("josdk.test.string", String.class)).hasValue("hello"); - } finally { - System.clearProperty("josdk.test.string"); - } - } - - @Test - void readsBooleanFromSystemProperty() { - System.setProperty("josdk.test.bool", "true"); - try { - var provider = new SystemPropertyConfigProvider(); - assertThat(provider.getValue("josdk.test.bool", Boolean.class)).hasValue(true); - } finally { - System.clearProperty("josdk.test.bool"); - } - } - - @Test - void readsIntegerFromSystemProperty() { - System.setProperty("josdk.test.integer", "42"); - try { - var provider = new SystemPropertyConfigProvider(); - assertThat(provider.getValue("josdk.test.integer", Integer.class)).hasValue(42); - } finally { - System.clearProperty("josdk.test.integer"); - } - } - - @Test - void readsLongFromSystemProperty() { - System.setProperty("josdk.test.long", "123456789"); - try { - var provider = new SystemPropertyConfigProvider(); - assertThat(provider.getValue("josdk.test.long", Long.class)).hasValue(123456789L); - } finally { - System.clearProperty("josdk.test.long"); - } - } - - @Test - void readsDurationFromSystemProperty() { - System.setProperty("josdk.test.duration", "PT30S"); - try { - var provider = new SystemPropertyConfigProvider(); - assertThat(provider.getValue("josdk.test.duration", Duration.class)) - .hasValue(Duration.ofSeconds(30)); - } finally { - System.clearProperty("josdk.test.duration"); - } - } - - @Test - void throwsForUnsupportedType() { - System.setProperty("josdk.test.unsupported", "value"); - try { - var provider = new SystemPropertyConfigProvider(); - assertThatIllegalArgumentException() - .isThrownBy(() -> provider.getValue("josdk.test.unsupported", AtomicInteger.class)) - .withMessageContaining("Unsupported config type"); - } finally { - System.clearProperty("josdk.test.unsupported"); - } - } -} From 556020628cfbcae272596146fda4e3425a022dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Thu, 26 Feb 2026 17:49:41 +0100 Subject: [PATCH 24/28] remove smalltye proof of concept from sample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- sample-operators/tomcat-operator/pom.xml | 23 ------------ .../operator/sample/TomcatOperator.java | 27 ++++---------- .../operator/sample/TomcatReconciler.java | 3 +- .../operator/sample/WebappReconciler.java | 3 +- .../SmallryeConfigProvider.java | 35 ------------------- 5 files changed, 9 insertions(+), 82 deletions(-) delete mode 100644 sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/smallryeconfig/SmallryeConfigProvider.java diff --git a/sample-operators/tomcat-operator/pom.xml b/sample-operators/tomcat-operator/pom.xml index c9afd825de..b7c3b05c98 100644 --- a/sample-operators/tomcat-operator/pom.xml +++ b/sample-operators/tomcat-operator/pom.xml @@ -39,13 +39,6 @@ pom import - - io.smallrye.config - smallrye-config-bom - 3.16.0 - pom - import - @@ -99,22 +92,6 @@ operator-framework-junit test - - - io.smallrye.config - smallrye-config - 3.11.4 - - - io.smallrye.config - smallrye-config-source-yaml - 3.11.4 - - - org.eclipse.microprofile.config - microprofile-config-api - 3.1 - diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java index 7cc5d9027e..bceaae8363 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java @@ -16,38 +16,25 @@ package io.javaoperatorsdk.operator.sample; import java.io.IOException; -import java.net.URL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.takes.facets.fork.FkRegex; import org.takes.facets.fork.TkFork; import org.takes.http.Exit; import org.takes.http.FtBasic; import io.javaoperatorsdk.operator.Operator; -import io.javaoperatorsdk.operator.config.loader.ConfigLoader; -import io.javaoperatorsdk.operator.sample.smallryeconfig.SmallryeConfigProvider; -import io.smallrye.config.SmallRyeConfigBuilder; -import io.smallrye.config.source.yaml.YamlConfigSource; public class TomcatOperator { + private static final Logger log = LoggerFactory.getLogger(TomcatOperator.class); + public static void main(String[] args) throws IOException { - URL configUrl = TomcatOperator.class.getResource("/application.yaml"); - if (configUrl == null) { - throw new IllegalStateException("application.yaml not found on classpath"); - } - var configLoader = - new ConfigLoader( - new SmallryeConfigProvider( - new SmallRyeConfigBuilder().withSources(new YamlConfigSource(configUrl)).build())); - - Operator operator = new Operator(configLoader.applyConfigs()); - operator.register( - new TomcatReconciler(), configLoader.applyControllerConfigs(TomcatReconciler.NAME)); - operator.register( - new WebappReconciler(operator.getKubernetesClient()), - configLoader.applyControllerConfigs(WebappReconciler.NAME)); + Operator operator = new Operator(); + operator.register(new TomcatReconciler()); + operator.register(new WebappReconciler(operator.getKubernetesClient())); operator.start(); new FtBasic(new TkFork(new FkRegex("/health", "ALL GOOD.")), 8080).start(Exit.NEVER); diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java index 6bb454eb13..d2fa9a021f 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java @@ -35,10 +35,9 @@ @Dependent(type = DeploymentDependentResource.class), @Dependent(type = ServiceDependentResource.class) }) -@ControllerConfiguration(name = TomcatReconciler.NAME) +@ControllerConfiguration public class TomcatReconciler implements Reconciler { - public static final String NAME = "tomcat"; private final Logger log = LoggerFactory.getLogger(getClass()); @Override diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java index 32d32f0a5b..5d362113ba 100644 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java +++ b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java @@ -47,11 +47,10 @@ import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; -@ControllerConfiguration(name = WebappReconciler.NAME) +@ControllerConfiguration public class WebappReconciler implements Reconciler, Cleaner { private static final Logger log = LoggerFactory.getLogger(WebappReconciler.class); - public static final String NAME = "webapp"; private final KubernetesClient kubernetesClient; diff --git a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/smallryeconfig/SmallryeConfigProvider.java b/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/smallryeconfig/SmallryeConfigProvider.java deleted file mode 100644 index b415566af2..0000000000 --- a/sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/smallryeconfig/SmallryeConfigProvider.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Java Operator SDK Authors - * - * Licensed 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 io.javaoperatorsdk.operator.sample.smallryeconfig; - -import java.util.Optional; - -import io.javaoperatorsdk.operator.config.loader.ConfigProvider; -import io.smallrye.config.SmallRyeConfig; - -public class SmallryeConfigProvider implements ConfigProvider { - - private final SmallRyeConfig smallRyeConfig; - - public SmallryeConfigProvider(SmallRyeConfig smallRyeConfig) { - this.smallRyeConfig = smallRyeConfig; - } - - @Override - public Optional getValue(String key, Class type) { - return smallRyeConfig.getOptionalValue(key, type); - } -} From b4ca3a3f56f2c836e73e9792d7af89880a040a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sat, 28 Feb 2026 11:21:48 +0100 Subject: [PATCH 25/28] fix naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- docs/content/en/docs/documentation/configuration.md | 6 +++--- .../operator/config/loader/ConfigLoader.java | 4 ++-- ...vider.java => AgregatePriorityListConfigProvider.java} | 4 ++-- .../operator/config/loader/ConfigLoaderTest.java | 2 +- .../loader/provider/PriorityListConfigProviderTest.java | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) rename operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/{AgregatePrirityListConfigProvider.java => AgregatePriorityListConfigProvider.java} (89%) diff --git a/docs/content/en/docs/documentation/configuration.md b/docs/content/en/docs/documentation/configuration.md index e761f7e1f5..34aa639525 100644 --- a/docs/content/en/docs/documentation/configuration.md +++ b/docs/content/en/docs/documentation/configuration.md @@ -189,7 +189,7 @@ Operator operator = new Operator(ConfigLoader.getDefault().applyConfigs()); | `EnvVarConfigProvider` | `System.getenv()` | dots and hyphens → underscores, upper-cased (`josdk.check-crd` → `JOSDK_CHECK_CRD`) | | `PropertiesConfigProvider` | `java.util.Properties` or `.properties` file | key used as-is; use `PropertiesConfigProvider.systemProperties()` to read Java system properties | | `YamlConfigProvider` | YAML file | dot-separated key traverses nested mappings | -| `AgregatePrirityListConfigProvider` | ordered list of providers | first non-empty result wins | +| `AgregatePriorityListConfigProvider` | ordered list of providers | first non-empty result wins | All string-based providers convert values to the target type automatically. Supported types: `String`, `Boolean`, `Integer`, `Long`, `Double`, `Duration` (ISO-8601, e.g. `PT30S`). @@ -244,11 +244,11 @@ Only keys that are actually present in the source are applied; everything else r programmatic or annotation-based default. You can also compose multiple sources with explicit priority using -`AgregatePrirityListConfigProvider`: +`AgregatePriorityListConfigProvider`: ```java var configLoader = new ConfigLoader( - new AgregatePrirityListConfigProvider(List.of( + new AgregatePriorityListConfigProvider(List.of( new EnvVarConfigProvider(), // highest priority PropertiesConfigProvider.systemProperties(), new YamlConfigProvider(Path.of("config/operator.yaml")) // lowest priority diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java index cc3a044112..1114fb87e6 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java @@ -27,7 +27,7 @@ import io.javaoperatorsdk.operator.api.config.ConfigurationServiceOverrider; import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; import io.javaoperatorsdk.operator.api.config.LeaderElectionConfigurationBuilder; -import io.javaoperatorsdk.operator.config.loader.provider.AgregatePrirityListConfigProvider; +import io.javaoperatorsdk.operator.config.loader.provider.AgregatePriorityListConfigProvider; import io.javaoperatorsdk.operator.config.loader.provider.EnvVarConfigProvider; import io.javaoperatorsdk.operator.config.loader.provider.PropertiesConfigProvider; import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; @@ -166,7 +166,7 @@ public static ConfigLoader getDefault() { public ConfigLoader() { this( - new AgregatePrirityListConfigProvider( + new AgregatePriorityListConfigProvider( List.of(new EnvVarConfigProvider(), PropertiesConfigProvider.systemProperties())), DEFAULT_CONTROLLER_KEY_PREFIX, DEFAULT_OPERATOR_KEY_PREFIX); diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/AgregatePrirityListConfigProvider.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/AgregatePriorityListConfigProvider.java similarity index 89% rename from operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/AgregatePrirityListConfigProvider.java rename to operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/AgregatePriorityListConfigProvider.java index 8da7d28f2f..5190156ce5 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/AgregatePrirityListConfigProvider.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/AgregatePriorityListConfigProvider.java @@ -24,11 +24,11 @@ * A {@link ConfigProvider} that delegates to an ordered list of providers. Providers are queried in * list order; the first non-empty result wins. */ -public class AgregatePrirityListConfigProvider implements ConfigProvider { +public class AgregatePriorityListConfigProvider implements ConfigProvider { private final List providers; - public AgregatePrirityListConfigProvider(List providers) { + public AgregatePriorityListConfigProvider(List providers) { this.providers = List.copyOf(providers); } diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java index 4fe9d50c2a..b238f5dee4 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java @@ -240,7 +240,7 @@ public Optional getValue(String key, Class type) { // -- binding coverage ------------------------------------------------------- /** - * Supported scalar types that AgregatePrirityListConfigProvider can parse from a string. Every + * Supported scalar types that AgregatePriorityListConfigProvider can parse from a string. Every * binding's type must be one of these. */ private static final Set> SUPPORTED_TYPES = diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java index c4678b9810..ad2a332868 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java @@ -35,7 +35,7 @@ private static PropertiesConfigProvider propsProvider(String key, String value) @Test void returnsEmptyWhenAllProvidersReturnEmpty() { var provider = - new AgregatePrirityListConfigProvider( + new AgregatePriorityListConfigProvider( List.of(new EnvVarConfigProvider(k -> null), propsProvider(null, null))); assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty(); } @@ -43,7 +43,7 @@ void returnsEmptyWhenAllProvidersReturnEmpty() { @Test void firstProviderWins() { var provider = - new AgregatePrirityListConfigProvider( + new AgregatePriorityListConfigProvider( List.of( new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_KEY") ? "first" : null), propsProvider("josdk.test.key", "second"))); @@ -53,7 +53,7 @@ void firstProviderWins() { @Test void fallsBackToLaterProviderWhenEarlierReturnsEmpty() { var provider = - new AgregatePrirityListConfigProvider( + new AgregatePriorityListConfigProvider( List.of( new EnvVarConfigProvider(k -> null), propsProvider("josdk.test.key", "from-second"))); @@ -66,7 +66,7 @@ void respectsOrderWithThreeProviders() { var second = propsProvider("josdk.test.key", "from-second"); var third = new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_KEY") ? "from-third" : null); - var provider = new AgregatePrirityListConfigProvider(List.of(first, second, third)); + var provider = new AgregatePriorityListConfigProvider(List.of(first, second, third)); assertThat(provider.getValue("josdk.test.key", String.class)).hasValue("from-second"); } } From 4716cabd7f49fb4c6d6b3f5fd77b048517ca9b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sat, 28 Feb 2026 11:23:34 +0100 Subject: [PATCH 26/28] cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../src/main/resources/application.yaml | 62 ------------------- 1 file changed, 62 deletions(-) delete mode 100644 sample-operators/tomcat-operator/src/main/resources/application.yaml diff --git a/sample-operators/tomcat-operator/src/main/resources/application.yaml b/sample-operators/tomcat-operator/src/main/resources/application.yaml deleted file mode 100644 index 128ee5eabc..0000000000 --- a/sample-operators/tomcat-operator/src/main/resources/application.yaml +++ /dev/null @@ -1,62 +0,0 @@ -# -# Copyright Java Operator SDK Authors -# -# Licensed 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. -# - -# JOSDK operator-level configuration (josdk.) -josdk: - check-crd: true - close-client-on-stop: true - use-ssa-to-patch-primary-resource: false - clone-secondary-resources-when-getting-from-cache: false - reconciliation: - termination-timeout: PT30S - concurrent-threads: 10 - workflow: - executor-threads: 10 - informer: - stop-on-error-during-startup: true - cache-sync-timeout: PT2M - dependent-resources: - ssa-based-create-update-match: true - - # Controller-level configuration (josdk.controller..) - controller: - tomcat: - finalizer: tomcat.sample.javaoperatorsdk.io/finalizer - generation-aware: true - label-selector: "" - max-reconciliation-interval: PT10M - field-manager: tomcat-controller - trigger-reconciler-on-all-events: false - informer-list-limit: 500 - retry: - max-attempts: 10 - initial-interval: 2000 - interval-multiplier: 1.5 - max-interval: 60000 - - webapp: - finalizer: webapp.sample.javaoperatorsdk.io/finalizer - generation-aware: true - label-selector: "" - max-reconciliation-interval: PT10M - field-manager: webapp-controller - trigger-reconciler-on-all-events: false - informer-list-limit: 500 - retry: - max-attempts: 10 - initial-interval: 2000 - interval-multiplier: 1.5 - max-interval: 60000 From e82b9630b1146033ff1206418514459ee5d2624f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sat, 28 Feb 2026 16:30:34 +0100 Subject: [PATCH 27/28] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../operator/config/loader/ConfigLoader.java | 3 +- .../config/loader/ConfigLoaderTest.java | 154 +++++++++++++++++- 2 files changed, 154 insertions(+), 3 deletions(-) diff --git a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java index 1114fb87e6..d46a6116d7 100644 --- a/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java +++ b/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java @@ -203,8 +203,7 @@ public Consumer applyConfigs() { /** * Returns a {@link Consumer} that applies every controller-level property found in the {@link * ConfigProvider} to the given {@link ControllerConfigurationOverrider}. The keys are looked up - * as {@code josdk.controller..}. Returns {@code null} when no binding - * has a matching value. + * as {@code josdk.controller..}. */ @SuppressWarnings("unchecked") public diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java index b238f5dee4..1fc1ebe98f 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/ConfigLoaderTest.java @@ -390,7 +390,159 @@ void leaderElectionMissingLeaseNameThrowsWhenOtherPropertiesPresent() { .withMessageContaining("lease-name"); } - /** Returns true when the two types are the same or one is the boxed/unboxed form of the other. */ + // -- retry ------------------------------------------------------------------ + + /** A minimal reconciler used to obtain a base ControllerConfiguration in retry tests. */ + @io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration + private static class DummyReconciler + implements io.javaoperatorsdk.operator.api.reconciler.Reconciler< + io.fabric8.kubernetes.api.model.ConfigMap> { + @Override + public io.javaoperatorsdk.operator.api.reconciler.UpdateControl< + io.fabric8.kubernetes.api.model.ConfigMap> + reconcile( + io.fabric8.kubernetes.api.model.ConfigMap r, + io.javaoperatorsdk.operator.api.reconciler.Context< + io.fabric8.kubernetes.api.model.ConfigMap> + ctx) { + return io.javaoperatorsdk.operator.api.reconciler.UpdateControl.noUpdate(); + } + } + + private static io.javaoperatorsdk.operator.api.config.ControllerConfiguration< + io.fabric8.kubernetes.api.model.ConfigMap> + baseControllerConfig() { + return new BaseConfigurationService().getConfigurationFor(new DummyReconciler()); + } + + private static io.javaoperatorsdk.operator.processing.retry.GenericRetry applyAndGetRetry( + java.util.function.Consumer< + io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider< + io.fabric8.kubernetes.api.model.ConfigMap>> + consumer) { + var overrider = + io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider.override( + baseControllerConfig()); + consumer.accept(overrider); + return (io.javaoperatorsdk.operator.processing.retry.GenericRetry) overrider.build().getRetry(); + } + + @Test + void retryIsNotConfiguredWhenNoRetryPropertiesPresent() { + var loader = new ConfigLoader(mapProvider(Map.of())); + var consumer = loader.applyControllerConfigs("ctrl"); + var overrider = + io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider.override( + baseControllerConfig()); + consumer.accept(overrider); + // no retry property set → retry stays at the controller's default (null or unchanged) + var result = overrider.build(); + // The consumer must not throw and the config is buildable + assertThat(result).isNotNull(); + } + + @Test + void retryQueriesExpectedKeys() { + var queriedKeys = new ArrayList(); + ConfigProvider recordingProvider = + new ConfigProvider() { + @Override + public Optional getValue(String key, Class type) { + queriedKeys.add(key); + return Optional.empty(); + } + }; + new ConfigLoader(recordingProvider).applyControllerConfigs("ctrl"); + assertThat(queriedKeys) + .contains( + "josdk.controller.ctrl.retry.max-attempts", + "josdk.controller.ctrl.retry.initial-interval", + "josdk.controller.ctrl.retry.interval-multiplier", + "josdk.controller.ctrl.retry.max-interval"); + } + + @Test + void retryMaxAttemptsIsApplied() { + var loader = + new ConfigLoader(mapProvider(Map.of("josdk.controller.ctrl.retry.max-attempts", 10))); + var retry = applyAndGetRetry(loader.applyControllerConfigs("ctrl")); + assertThat(retry.getMaxAttempts()).isEqualTo(10); + // other fields stay at their defaults + assertThat(retry.getInitialInterval()) + .isEqualTo( + io.javaoperatorsdk.operator.processing.retry.GenericRetry + .defaultLimitedExponentialRetry() + .getInitialInterval()); + } + + @Test + void retryInitialIntervalIsApplied() { + var loader = + new ConfigLoader(mapProvider(Map.of("josdk.controller.ctrl.retry.initial-interval", 500L))); + var retry = applyAndGetRetry(loader.applyControllerConfigs("ctrl")); + assertThat(retry.getInitialInterval()).isEqualTo(500L); + } + + @Test + void retryIntervalMultiplierIsApplied() { + var loader = + new ConfigLoader( + mapProvider(Map.of("josdk.controller.ctrl.retry.interval-multiplier", 2.0))); + var retry = applyAndGetRetry(loader.applyControllerConfigs("ctrl")); + assertThat(retry.getIntervalMultiplier()).isEqualTo(2.0); + } + + @Test + void retryMaxIntervalIsApplied() { + var loader = + new ConfigLoader(mapProvider(Map.of("josdk.controller.ctrl.retry.max-interval", 30000L))); + var retry = applyAndGetRetry(loader.applyControllerConfigs("ctrl")); + assertThat(retry.getMaxInterval()).isEqualTo(30000L); + } + + @Test + void retryAllPropertiesApplied() { + var values = new HashMap(); + values.put("josdk.controller.ctrl.retry.max-attempts", 7); + values.put("josdk.controller.ctrl.retry.initial-interval", 1000L); + values.put("josdk.controller.ctrl.retry.interval-multiplier", 3.0); + values.put("josdk.controller.ctrl.retry.max-interval", 60000L); + var loader = new ConfigLoader(mapProvider(values)); + var retry = applyAndGetRetry(loader.applyControllerConfigs("ctrl")); + assertThat(retry.getMaxAttempts()).isEqualTo(7); + assertThat(retry.getInitialInterval()).isEqualTo(1000L); + assertThat(retry.getIntervalMultiplier()).isEqualTo(3.0); + assertThat(retry.getMaxInterval()).isEqualTo(60000L); + } + + @Test + void retryStartsFromDefaultLimitedExponentialRetryDefaults() { + // Only max-attempts is overridden — other fields must still be the defaults. + var defaults = + io.javaoperatorsdk.operator.processing.retry.GenericRetry.defaultLimitedExponentialRetry(); + var loader = + new ConfigLoader(mapProvider(Map.of("josdk.controller.ctrl.retry.max-attempts", 3))); + var retry = applyAndGetRetry(loader.applyControllerConfigs("ctrl")); + assertThat(retry.getMaxAttempts()).isEqualTo(3); + assertThat(retry.getInitialInterval()).isEqualTo(defaults.getInitialInterval()); + assertThat(retry.getIntervalMultiplier()).isEqualTo(defaults.getIntervalMultiplier()); + assertThat(retry.getMaxInterval()).isEqualTo(defaults.getMaxInterval()); + } + + @Test + void retryIsIsolatedPerControllerName() { + var values = new HashMap(); + values.put("josdk.controller.alpha.retry.max-attempts", 4); + values.put("josdk.controller.beta.retry.max-attempts", 9); + var loader = new ConfigLoader(mapProvider(values)); + + var alphaRetry = applyAndGetRetry(loader.applyControllerConfigs("alpha")); + var betaRetry = applyAndGetRetry(loader.applyControllerConfigs("beta")); + + assertThat(alphaRetry.getMaxAttempts()).isEqualTo(4); + assertThat(betaRetry.getMaxAttempts()).isEqualTo(9); + } + private static boolean isTypeCompatible(Class methodParam, Class bindingType) { if (methodParam == bindingType) return true; if (methodParam == boolean.class && bindingType == Boolean.class) return true; From 34a249f8a607facd27950ebd6626302894fc043d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Sat, 28 Feb 2026 18:05:05 +0100 Subject: [PATCH 28/28] integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../baseapi/configloader/ConfigLoaderIT.java | 146 ++++++++++++++++++ .../ConfigLoaderTestCustomResource.java | 30 ++++ .../ConfigLoaderTestCustomResourceStatus.java | 35 +++++ .../ConfigLoaderTestReconciler.java | 58 +++++++ 4 files changed, 269 insertions(+) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestCustomResourceStatus.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestReconciler.java diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderIT.java new file mode 100644 index 0000000000..d1ee0afa59 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderIT.java @@ -0,0 +1,146 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.baseapi.configloader; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider; +import io.javaoperatorsdk.operator.config.loader.ConfigLoader; +import io.javaoperatorsdk.operator.config.loader.ConfigProvider; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.support.TestUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * Integration tests that verify {@link ConfigLoader} property overrides take effect when wiring up + * a real operator instance via {@link LocallyRunOperatorExtension}. + * + *

Each nested class exercises a distinct group of properties so that failures are easy to + * pinpoint. + */ +class ConfigLoaderIT { + + /** Builds a {@link ConfigProvider} backed by a plain map. */ + private static ConfigProvider mapProvider(Map values) { + return new ConfigProvider() { + @Override + @SuppressWarnings("unchecked") + public Optional getValue(String key, Class type) { + return Optional.ofNullable((T) values.get(key)); + } + }; + } + + // --------------------------------------------------------------------------- + // Operator-level properties + // --------------------------------------------------------------------------- + + @Nested + class OperatorLevelProperties { + + /** + * Verifies that {@code josdk.reconciliation.concurrent-threads} loaded via {@link ConfigLoader} + * and applied through {@code withConfigurationService} actually changes the operator's thread + * pool size. + */ + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder() + .withReconciler(new ConfigLoaderTestReconciler(0)) + .withConfigurationService( + new ConfigLoader(mapProvider(Map.of("josdk.reconciliation.concurrent-threads", 2))) + .applyConfigs()) + .build(); + + @Test + void concurrentReconciliationThreadsIsAppliedFromConfigLoader() { + assertThat(operator.getOperator().getConfigurationService().concurrentReconciliationThreads()) + .isEqualTo(2); + } + } + + // --------------------------------------------------------------------------- + // Controller-level retry + // --------------------------------------------------------------------------- + + @Nested + class ControllerRetryProperties { + + static final int FAILS = 2; + // controller name is the lower-cased simple class name by default + static final String CTRL_NAME = ConfigLoaderTestReconciler.class.getSimpleName().toLowerCase(); + + /** + * Verifies that retry properties read by {@link ConfigLoader} for a specific controller name + * are applied when registering the reconciler via a {@code configurationOverrider} consumer, + * and that the resulting operator actually retries and eventually succeeds. + */ + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder() + .withReconciler( + new ConfigLoaderTestReconciler(FAILS), + // applyControllerConfigs returns Consumer>; + // withReconciler takes the raw Consumer + (Consumer) + (Consumer) + new ConfigLoader( + mapProvider( + Map.of( + "josdk.controller." + CTRL_NAME + ".retry.max-attempts", + 5, + "josdk.controller." + CTRL_NAME + ".retry.initial-interval", + 100L))) + .applyControllerConfigs(CTRL_NAME)) + .build(); + + @Test + void retryConfigFromConfigLoaderIsAppliedAndReconcilerEventuallySucceeds() { + var resource = createResource("1"); + operator.create(resource); + + await("reconciler succeeds after retries") + .atMost(10, TimeUnit.SECONDS) + .pollInterval(100, TimeUnit.MILLISECONDS) + .untilAsserted( + () -> { + assertThat(TestUtils.getNumberOfExecutions(operator)).isEqualTo(FAILS + 1); + var updated = + operator.get( + ConfigLoaderTestCustomResource.class, resource.getMetadata().getName()); + assertThat(updated.getStatus()).isNotNull(); + assertThat(updated.getStatus().getState()) + .isEqualTo(ConfigLoaderTestCustomResourceStatus.State.SUCCESS); + }); + } + + private ConfigLoaderTestCustomResource createResource(String id) { + var resource = new ConfigLoaderTestCustomResource(); + resource.setMetadata(new ObjectMetaBuilder().withName("cfgloader-retry-" + id).build()); + return resource; + } + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestCustomResource.java new file mode 100644 index 0000000000..a892b2391d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestCustomResource.java @@ -0,0 +1,30 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.baseapi.configloader; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.Kind; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@Kind("ConfigLoaderSample") +@ShortNames("cls") +public class ConfigLoaderTestCustomResource + extends CustomResource implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestCustomResourceStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestCustomResourceStatus.java new file mode 100644 index 0000000000..c70202bb73 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestCustomResourceStatus.java @@ -0,0 +1,35 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.baseapi.configloader; + +public class ConfigLoaderTestCustomResourceStatus { + + public enum State { + SUCCESS, + ERROR + } + + private State state; + + public State getState() { + return state; + } + + public ConfigLoaderTestCustomResourceStatus setState(State state) { + this.state = state; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestReconciler.java new file mode 100644 index 0000000000..dbadfd4414 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/baseapi/configloader/ConfigLoaderTestReconciler.java @@ -0,0 +1,58 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed 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 io.javaoperatorsdk.operator.baseapi.configloader; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +/** + * A reconciler that fails for the first {@code numberOfFailures} invocations and then succeeds, + * setting the status to {@link ConfigLoaderTestCustomResourceStatus.State#SUCCESS}. + */ +@ControllerConfiguration +public class ConfigLoaderTestReconciler + implements Reconciler, TestExecutionInfoProvider { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + private final int numberOfFailures; + + public ConfigLoaderTestReconciler(int numberOfFailures) { + this.numberOfFailures = numberOfFailures; + } + + @Override + public UpdateControl reconcile( + ConfigLoaderTestCustomResource resource, Context context) { + int execution = numberOfExecutions.incrementAndGet(); + if (execution <= numberOfFailures) { + throw new RuntimeException("Simulated failure on execution " + execution); + } + var status = new ConfigLoaderTestCustomResourceStatus(); + status.setState(ConfigLoaderTestCustomResourceStatus.State.SUCCESS); + resource.setStatus(status); + return UpdateControl.patchStatus(resource); + } + + @Override + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } +}