Skip to content

feat: configuration adapters#3177

Open
csviri wants to merge 24 commits intooperator-framework:nextfrom
csviri:config-loading
Open

feat: configuration adapters#3177
csviri wants to merge 24 commits intooperator-framework:nextfrom
csviri:config-loading

Conversation

@csviri
Copy link
Collaborator

@csviri csviri commented Feb 22, 2026

Abstraction and implementation to load configurations from env and system properties.

@openshift-ci openshift-ci bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Feb 22, 2026
@csviri csviri linked an issue Feb 22, 2026 that may be closed by this pull request
@csviri csviri changed the title feat: configuration adapter feat: configuration adapters Feb 22, 2026
@csviri csviri changed the title feat: configuration adapters [WIP]feat: configuration adapters Feb 22, 2026
@csviri
Copy link
Collaborator Author

csviri commented Feb 23, 2026

@xstefank @metacosm pls take a look on this (description in related issue).

  1. Maybe we should have this set by default in the background for an operator?
  2. Not sure if we need also ConfigLoader as an interface, now only the ConfigProvider is abstracted away, but maybe we need both to cover all possible extension cases.

thank you!

@csviri csviri changed the title [WIP]feat: configuration adapters feat: configuration adapters Feb 23, 2026
@csviri csviri marked this pull request as ready for review February 23, 2026 15:20
Copilot AI review requested due to automatic review settings February 23, 2026 15:20
@openshift-ci openshift-ci bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Feb 23, 2026
@openshift-ci openshift-ci bot requested review from metacosm and xstefank February 23, 2026 15:20
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a configuration-loading abstraction (env vars + system properties) and wires it into the sample Tomcat operator to apply operator- and controller-level configuration overrides.

Changes:

  • Added ConfigProvider/DefaultConfigProvider plus ConfigLoader to bind config keys to configuration overriders.
  • Updated sample reconcilers/operators to use explicit controller names and apply controller-specific configs.
  • Added unit tests covering config provider conversion and loader binding behavior.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/WebappReconciler.java Adds an explicit controller name constant and uses it in @ControllerConfiguration.
sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatReconciler.java Adds an explicit controller name constant and uses it in @ControllerConfiguration.
sample-operators/tomcat-operator/src/main/java/io/javaoperatorsdk/operator/sample/TomcatOperator.java Uses ConfigLoader to apply operator/controller configuration at startup.
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigProvider.java Introduces a typed key/value config provider abstraction.
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProvider.java Implements provider backed by env vars + system properties with type conversion.
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBinding.java Adds a binding primitive to connect keys, types, and overrider setters.
operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoader.java Implements binding-driven application of operator/controller config, including retry overrides.
operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/DefaultConfigProviderTest.java Tests env/sysprop precedence and value conversion.
operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigLoaderTest.java Tests operator/controller config application and key construction.
operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/loader/ConfigBindingTest.java Tests ConfigBinding stores key/type/setter correctly.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 34 to 38
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));
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfigLoader.applyConfigs() / applyControllerConfigs(...) can return null (by design per tests). Passing these directly into Operator(...) / operator.register(..., ...) makes the sample rely on downstream null-handling and makes it easy to introduce NPEs when composing consumers. Consider changing the ConfigLoader API to return a no-op Consumer when nothing is configured (or alternatively return Optional<Consumer<...>>) so call sites can be null-free.

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +98
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";
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry-related keys don’t document units/semantics for interval values (e.g., are the Long values milliseconds? seconds?). Add Javadoc near these suffix constants (or on buildRetryConsumer) that clearly states expected units and acceptable ranges; alternatively consider using Duration for interval properties (and converting to whatever GenericRetry expects) for consistency with other duration configs.

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +177
// Cast is safe: the setter BiConsumer<ControllerConfigurationOverrider<?>, T> is covariant in
// its first parameter for our usage – we only ever call it with
// ControllerConfigurationOverrider<R>.
List<ConfigBinding<ControllerConfigurationOverrider<R>, ?>> bindings =
(List<ConfigBinding<ControllerConfigurationOverrider<R>, ?>>) (List<?>) CONTROLLER_BINDINGS;
Consumer<ControllerConfigurationOverrider<R>> consumer = buildConsumer(bindings, prefix);

Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unchecked double-cast here is a maintenance hazard and the comment mentions covariance, but BiConsumer is invariant in its type parameters. A safer approach is to adjust ConfigBinding to accept BiConsumer<? super O, ? super T> and/or define CONTROLLER_BINDINGS with a type that doesn’t require casting (e.g., keep it raw/erased in one place and only cast the final Consumer once), minimizing the scope of unchecked operations.

Suggested change
// Cast is safe: the setter BiConsumer<ControllerConfigurationOverrider<?>, T> is covariant in
// its first parameter for our usage – we only ever call it with
// ControllerConfigurationOverrider<R>.
List<ConfigBinding<ControllerConfigurationOverrider<R>, ?>> bindings =
(List<ConfigBinding<ControllerConfigurationOverrider<R>, ?>>) (List<?>) CONTROLLER_BINDINGS;
Consumer<ControllerConfigurationOverrider<R>> consumer = buildConsumer(bindings, prefix);
// Build the consumer from the shared controller bindings using a raw list to avoid
// propagating unchecked generic casts. We then narrow it in a single place below.
Consumer<?> rawConsumer = buildConsumer((List) CONTROLLER_BINDINGS, prefix);
Consumer<ControllerConfigurationOverrider<R>> consumer =
(Consumer<ControllerConfigurationOverrider<R>>) rawConsumer;

Copilot uses AI. Check for mistakes.
Comment on lines +191 to +200
private <R extends HasMetadata> Consumer<ControllerConfigurationOverrider<R>> buildRetryConsumer(
String prefix) {
Optional<Integer> maxAttempts =
configProvider.getValue(prefix + RETRY_MAX_ATTEMPTS_SUFFIX, Integer.class);
Optional<Long> initialInterval =
configProvider.getValue(prefix + RETRY_INITIAL_INTERVAL_SUFFIX, Long.class);
Optional<Double> intervalMultiplier =
configProvider.getValue(prefix + RETRY_INTERVAL_MULTIPLIER_SUFFIX, Double.class);
Optional<Long> maxInterval =
configProvider.getValue(prefix + RETRY_MAX_INTERVAL_SUFFIX, Long.class);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Retry override behavior is new but isn’t covered by tests yet. Add unit tests that set one or more retry keys and assert that the returned controller consumer calls withRetry(...) and that the resulting GenericRetry instance reflects only the configured overrides (including Double parsing for retry.interval-multiplier).

Copilot uses AI. Check for mistakes.
Comment on lines 41 to 42
public static final String TOMCAT_CONTROLLER_NAME = "tomcat";
private final Logger log = LoggerFactory.getLogger(getClass());
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation here is inconsistent with the rest of the class (extra leading spaces). Align these members with the project’s standard formatting to keep diffs clean and avoid style/checkstyle issues.

Suggested change
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());

Copilot uses AI. Check for mistakes.
@metacosm
Copy link
Collaborator

To be honest, I'm not sure this is something we want to support.

For one thing, this makes the code less understandable, imo.

Also, there are already lots of configuration mechanisms and while I understand the intent behind this PR, creating a wrapper for these mechanisms is not straightforward and will almost inevitably lead to feature creep and/or implementation leaks as users will most likely want to support specific config mechanism features.

Another issue is also maintenance where lots of values are hardcoded which would make refactoring more difficult. Also, if a new configuration option is added, one will have to remember to add it to the ConfigLoader implementation(s).

Finally, I'm not convinced this actually answers a need from the community. Has there been users asking for this? It seems that downstream clients of the SDK are already providing native support for their own configuration systems which, I think, would feel more natural to their users.

@csviri
Copy link
Collaborator Author

csviri commented Feb 23, 2026

Also, there are already lots of configuration mechanisms and while I understand the intent behind this PR, creating a wrapper for these mechanisms is not straightforward and will almost inevitably lead to feature creep and/or implementation leaks as users will most likely want to support specific config mechanism features.

No that is not the intention; from the issue:

Out of the box, we can provide an implementation (no external dependency) that reads environment variables and system properties. Mainly a naming scheme.

So the only mechanism I want to support in this repo is what is in this PR.

The issue is that I'm now maintaining a few operators, and for each, we would like to fine-tune some configurations in prod using environment variables (very common in k8s deployments). But now there is no way to do it out of the box with core JOSDK. So I would like to have an out of the box solution that where it is easy to make: like a one liner as in this example usage in the PR.

So it is quite painful now since I have to replicate the same logic accross multiple operators (read the env variables and handle it to config overrider), while this is a generic problem. That is why Spring + Quarkus extensions solve this out of the box, but for some projects, we simply cannot use those frameworks, unfortunately.

Another issue is also maintenance where lots of values are hardcoded which would make refactoring more difficult. Also, if a new configuration option is added, one will have to remember to add it to the ConfigLoader implementation(s).

yes but that is rare, also I can add a unit tests that checks that dynamically. It is actually not that painful.

@metacosm
Copy link
Collaborator

metacosm commented Feb 23, 2026

So the only mechanism I want to support in this repo is what is in this PR.

So the issue is that I'm maintaining now a few operators, and for each, we would like to fine-tune some configurations in prod with environment variables (very common in k8s deployments). But now there is no way to do it out of the box with core JOSDK. So I would like to have an out of the box solution that where it is easy to make: like a one liner as in this example usage in the PR.

This could be easily solved by a small lib that would do pretty much what is in this PR that could be shared among your operators, without having that code in the SDK itself. At the very least, this shouldn't be part of the core module if people think this is a useful addition to the SDK.

So it is quite painful now since I have to replicate the same logic accross multiple operators, while this is a generic problem. That is why Spring + Quarkus extensions solve this out of the box, but for some projects, we simply cannot use those frameworks, unfortunately.

The problem is that with this PR there will be a competing configuration mechanism with what frameworks support, adding a potential source of confusion for users. Also, what should happen if a user mistakenly uses the default ConfigLoader in addition to a framework-native solution? This might lead to unexpected results.

Another issue is also maintenance where lots of values are hardcoded which would make refactoring more difficult. Also, if a new configuration option is added, one will have to remember to add it to the ConfigLoader implementation(s).

yes but that is rare, also I can add a unit tests that checks that dynamically. It is actually not that painful.

If we decide to go ahead with this, please add such a test.

@csviri
Copy link
Collaborator Author

csviri commented Feb 23, 2026

This could be easily solved by a small lib that would do pretty much what is in this PR that could be shared among your operators, without having that code in the SDK itself. At the very least, this shouldn't be part of the core module if people think this is a useful addition to the SDK.

We could say that also to quarkus extension. Why not to solve a common subproblem in the core, that is what framework is about?

he problem is that with this PR there will be a competing configuration mechanism with what frameworks support, adding a potential source of confusion for users. Also, what should happen if a user mistakenly uses the default ConfigLoader in addition to a framework-native solution? This might lead to unexpected results.

In the current form it does not, it builds on top of it:

    var configLoader = ConfigLoader.getDefault();
    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.start();

It uses the same configuration mechanism that you would write on top of current config approach. How do you see this breaking anything? pls elaborate

@csviri
Copy link
Collaborator Author

csviri commented Feb 23, 2026

I added a unit test to check if we cover all the methods.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 151 to 155
/**
* 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 behavior.
*/
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JavaDoc incorrectly states "Returns {@code null} when no binding has a matching value". The implementation on line 157 calls buildConsumer which always returns a non-null Consumer (line 236 returns a no-op consumer o -> {} when no values are found). The JavaDoc should be updated to reflect that this method always returns a non-null Consumer.

Copilot uses AI. Check for mistakes.
Comment on lines +160 to +165
/**
* 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.<controllerName>.<property>}. Returns {@code null} when no binding
* has a matching value.
*/
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JavaDoc states that this method "Returns {@code null} when no binding has a matching value", but the implementation always returns a non-null Consumer. When no bindings match, buildConsumer returns a no-op consumer (o -> {}), not null. The JavaDoc should be updated to reflect that this method always returns a non-null Consumer, which may be a no-op if no configuration values are found.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 88 to 100
} 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());
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The convert method only handles boxed types (Boolean.class, Integer.class, etc.) but not primitive types (boolean.class, int.class, etc.). While Java's auto-boxing handles this when calling methods that accept primitives, the method should also handle primitive types explicitly for completeness and to match the test expectations in ConfigLoaderTest.SUPPORTED_TYPES (lines 222-233) which includes both boxed and primitive types. Consider adding explicit handling for primitive types to make the implementation more robust and self-documenting.

Suggested change
} 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());
}
} else if (type == Boolean.class || type == boolean.class) {
converted = Boolean.parseBoolean(raw);
} else if (type == Integer.class || type == int.class) {
converted = Integer.parseInt(raw);
} else if (type == Long.class || type == long.class) {
converted = Long.parseLong(raw);
} else if (type == Double.class || 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());
}
if (type.isPrimitive()) {
@SuppressWarnings("unchecked")
final T result = (T) converted;
return result;
}

Copilot uses AI. Check for mistakes.
Comment on lines 60 to 66
public <T> Optional<T> getValue(String key, Class<T> type) {
String raw = resolveRaw(key);
if (raw == null) {
return Optional.empty();
}
return Optional.of(convert(raw, type));
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When parsing fails (e.g., Integer.parseInt throws NumberFormatException for invalid integer values, or Duration.parse throws DateTimeParseException for invalid duration format), the exception will bubble up without context about which configuration key caused the problem. Consider wrapping parse exceptions with more informative error messages. This could be done by modifying getValue to catch parse exceptions from convert and re-throw them with the key name included, helping users diagnose configuration errors more easily.

Copilot uses AI. Check for mistakes.
Comment on lines 68 to 78
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);
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System.getProperty can return an empty string for a property that exists but has no value. Currently, an empty string will be passed to convert, which will throw a parsing exception for numeric types and Duration. Consider treating empty strings the same as null values by returning Optional.empty(), as empty strings are typically not valid configuration values and would just cause parsing errors.

Copilot uses AI. Check for mistakes.
Comment on lines 74 to 77
if (envValue != null) {
return envValue;
}
return System.getProperty(key);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Environment variables can also be set to empty strings. Similar to the system property case, empty strings should likely be treated as absent values by checking if the returned envValue is empty and returning Optional.empty() in that case, to avoid parsing errors.

Suggested change
if (envValue != null) {
return envValue;
}
return System.getProperty(key);
if (envValue != null && !envValue.isEmpty()) {
return envValue;
}
String propertyValue = System.getProperty(key);
if (propertyValue != null && !propertyValue.isEmpty()) {
return propertyValue;
}
return null;

Copilot uses AI. Check for mistakes.
Comment on lines +184 to +215
/**
* 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 <R extends HasMetadata> Consumer<ControllerConfigurationOverrider<R>> buildRetryConsumer(
String prefix) {
Optional<Integer> maxAttempts =
configProvider.getValue(prefix + RETRY_MAX_ATTEMPTS_SUFFIX, Integer.class);
Optional<Long> initialInterval =
configProvider.getValue(prefix + RETRY_INITIAL_INTERVAL_SUFFIX, Long.class);
Optional<Double> intervalMultiplier =
configProvider.getValue(prefix + RETRY_INTERVAL_MULTIPLIER_SUFFIX, Double.class);
Optional<Long> 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);
};
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retry configuration logic (buildRetryConsumer method) lacks test coverage. Consider adding tests that verify retry properties (retry.max-attempts, retry.initial-interval, retry.interval-multiplier, retry.max-interval) are correctly loaded from the ConfigProvider and applied to the ControllerConfigurationOverrider. This would ensure the retry configuration works as expected and prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +169
Consumer<ControllerConfigurationOverrider<R>> applyControllerConfigs(String controllerName) {
String prefix = controllerKeyPrefix + controllerName + ".";
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method does not validate that controllerName is not null. If null is passed, a NullPointerException will be thrown at line 169 when concatenating strings. Consider adding a null check with an appropriate exception message, such as: Objects.requireNonNull(controllerName, "controllerName must not be null").

Copilot uses AI. Check for mistakes.
Comment on lines +163 to +164
* as {@code josdk.controller.<controllerName>.<property>}. Returns {@code null} when no binding
* has a matching value.
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The javadoc states "Returns {@code null} when no binding has a matching value" but this is incorrect. The method always returns a non-null Consumer. When no properties are configured, it returns a no-op consumer (from buildConsumer at line 236). The javadoc should be updated to reflect that this method always returns a non-null Consumer, which may be a no-op consumer if no configuration properties are found.

Suggested change
* as {@code josdk.controller.<controllerName>.<property>}. Returns {@code null} when no binding
* has a matching value.
* as {@code josdk.controller.<controllerName>.<property>}. This method never returns
* {@code null}; if no binding has a matching value, it returns a no-op consumer.

Copilot uses AI. Check for mistakes.
@metacosm
Copy link
Collaborator

This could be easily solved by a small lib that would do pretty much what is in this PR that could be shared among your operators, without having that code in the SDK itself. At the very least, this shouldn't be part of the core module if people think this is a useful addition to the SDK.

We could say that also to quarkus extension. Why not to solve a common subproblem in the core, that is what framework is about?

Except that JOSDK is a SDK, not a framework, or at least, it shouldn't be, despite already providing more things than a simple SDK.

As I said, if people think that this is a valuable addition, at the very least, it shouldn't be in the core module so that it requires users to make an explicit decision to use that configuration option to make things less confusing for people who consume the SDK via a framework which already provide native configuration options.

he problem is that with this PR there will be a competing configuration mechanism with what frameworks support, adding a potential source of confusion for users. Also, what should happen if a user mistakenly uses the default ConfigLoader in addition to a framework-native solution? This might lead to unexpected results.

In the current form it does not, it builds on top of it:

    var configLoader = ConfigLoader.getDefault();
    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.start();

It uses the same configuration mechanism that you would write on top of current config approach. How do you see this breaking anything? pls elaborate

If you use the Quarkus extension which provides an external configuration mechanism but also use the code above, which, granted, should not happen, the resulting configuration might not be trivial to understand. However, the SDK documentation will state that you can configure things this way, while the Quarkus configuration (or Spring for that matter) might be done another way, leading to user confusion.

@csviri
Copy link
Collaborator Author

csviri commented Feb 23, 2026

Except that JOSDK is a SDK, not a framework, or at least, it shouldn't be, despite already providing more things than a simple SDK.

@metacosm I really don't want to into idealogical disucssion, also things that are matter of definitions, but I think if enough if I say that if you take a look on the modules all core modules start with "operator-framework*" :) no offense.
Also part of JOSDK is also josdk-webhooks project and some others.

it shouldn't be in the core module so that it requires users to make an explicit decision to use that configuration option to make things less confusing for people who consume the SDK via a framework which already provide native configuration options.

But users already have to write explicit code, that is why I asked your and others opnion here:
#3177 (comment)
If we should go further.
So it is not like is a feature switch,users have to explicitly write code to add it. I don't think it would create any confusion.

while the Quarkus configuration (or Spring for that matter) might be done another way, leading to user confusion.

Yes would be nice to sync the naming, but not sure if that is possible at this point. But if we explain this in the docs I think this is what users will easily understand.

it shouldn't be in the core module

If also others think that this should be a separate module fine. But shouldn't we than also separate for example dependent resource as a separate module for v6, since users have to make a decision to use it or not?

@metacosm
Copy link
Collaborator

I am not going to spend any more time on this as I don't think this feature should be part of JOSDK. That said, as long as the core module is not impacted, I'm reluctantly OK with its inclusion if I'm the only one who thinks this shouldn't be part of JOSDK. I just hope that this won't be a source of bugs in the future…

csviri and others added 22 commits February 26, 2026 17:11
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Chris Laprun <metacosm@gmail.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
…tor/api/config/loader/ConfigLoader.java

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 15 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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));
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name contains a spelling error: "AgregatePrirityListConfigProvider" should be "AggregatePriorityListConfigProvider". Note the misspellings: "Agregate" should be "Aggregate" and "Pririty" should be "Priority".

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +31
public class AgregatePrirityListConfigProvider implements ConfigProvider {

private final List<ConfigProvider> providers;

public AgregatePrirityListConfigProvider(List<ConfigProvider> providers) {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name contains a spelling error: "AgregatePrirityListConfigProvider" should be "AggregatePriorityListConfigProvider". Note the misspellings: "Agregate" should be "Aggregate" and "Pririty" should be "Priority".

Suggested change
public class AgregatePrirityListConfigProvider implements ConfigProvider {
private final List<ConfigProvider> providers;
public AgregatePrirityListConfigProvider(List<ConfigProvider> providers) {
public class AggregatePriorityListConfigProvider implements ConfigProvider {
private final List<ConfigProvider> providers;
public AggregatePriorityListConfigProvider(List<ConfigProvider> providers) {

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +31
public class AgregatePrirityListConfigProvider implements ConfigProvider {

private final List<ConfigProvider> providers;

public AgregatePrirityListConfigProvider(List<ConfigProvider> providers) {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name contains a spelling error: "AgregatePrirityListConfigProvider" should be "AggregatePriorityListConfigProvider". Note the misspellings: "Agregate" should be "Aggregate" and "Pririty" should be "Priority".

Suggested change
public class AgregatePrirityListConfigProvider implements ConfigProvider {
private final List<ConfigProvider> providers;
public AgregatePrirityListConfigProvider(List<ConfigProvider> providers) {
public class AggregatePriorityListConfigProvider implements ConfigProvider {
private final List<ConfigProvider> providers;
public AggregatePriorityListConfigProvider(List<ConfigProvider> providers) {

Copilot uses AI. Check for mistakes.
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;
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The import uses a misspelled class name: "AgregatePrirityListConfigProvider" should be "AggregatePriorityListConfigProvider". Note the misspellings: "Agregate" should be "Aggregate" and "Pririty" should be "Priority".

Suggested change
import io.javaoperatorsdk.operator.config.loader.provider.AgregatePrirityListConfigProvider;
import io.javaoperatorsdk.operator.config.loader.provider.AggregatePriorityListConfigProvider;

Copilot uses AI. Check for mistakes.
Comment on lines +247 to +251
`AgregatePrirityListConfigProvider`:

```java
var configLoader = new ConfigLoader(
new AgregatePrirityListConfigProvider(List.of(
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name in the code example contains a spelling error: "AgregatePrirityListConfigProvider" should be "AggregatePriorityListConfigProvider". Note the misspellings: "Agregate" should be "Aggregate" and "Pririty" should be "Priority".

Suggested change
`AgregatePrirityListConfigProvider`:
```java
var configLoader = new ConfigLoader(
new AgregatePrirityListConfigProvider(List.of(
`AggregatePriorityListConfigProvider`:
```java
var configLoader = new ConfigLoader(
new AggregatePriorityListConfigProvider(List.of(

Copilot uses AI. Check for mistakes.

public ConfigLoader() {
this(
new AgregatePrirityListConfigProvider(
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name contains a spelling error: "AgregatePrirityListConfigProvider" should be "AggregatePriorityListConfigProvider". Note the misspellings: "Agregate" should be "Aggregate" and "Pririty" should be "Priority".

Copilot uses AI. Check for mistakes.
Comment on lines +247 to +251
`AgregatePrirityListConfigProvider`:

```java
var configLoader = new ConfigLoader(
new AgregatePrirityListConfigProvider(List.of(
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name in the documentation contains a spelling error: "AgregatePrirityListConfigProvider" should be "AggregatePriorityListConfigProvider". Note the misspellings: "Agregate" should be "Aggregate" and "Pririty" should be "Priority".

Suggested change
`AgregatePrirityListConfigProvider`:
```java
var configLoader = new ConfigLoader(
new AgregatePrirityListConfigProvider(List.of(
`AggregatePriorityListConfigProvider`:
```java
var configLoader = new ConfigLoader(
new AggregatePriorityListConfigProvider(List.of(

Copilot uses AI. Check for mistakes.
max-reconciliation-interval: PT10M
field-manager: webapp-controller
trigger-reconciler-on-all-events: false
informer-list-limit: 500
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent key naming: The YAML file uses "informer-list-limit" (with a hyphen), but the ConfigLoader binding at line 161 of ConfigLoader.java expects "informer.list-limit" (with a dot after "informer"). This inconsistency means the configuration value in this YAML file will not be loaded. Change to "informer.list-limit" or update the binding to use the hyphenated version.

Copilot uses AI. Check for mistakes.
@Test
void fallsBackToLaterProviderWhenEarlierReturnsEmpty() {
var provider =
new AgregatePrirityListConfigProvider(
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name contains a spelling error: "AgregatePrirityListConfigProvider" should be "AggregatePriorityListConfigProvider". Note the misspellings: "Agregate" should be "Aggregate" and "Pririty" should be "Priority".

Copilot uses AI. Check for mistakes.
"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");
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test "applyControllerConfigsQueriesAllExpectedPropertySuffixes" does not verify that retry configuration keys are queried. The test should also assert that the following keys are queried: "josdk.controller.ctrl.retry.max-attempts", "josdk.controller.ctrl.retry.initial-interval", "josdk.controller.ctrl.retry.interval-multiplier", and "josdk.controller.ctrl.retry.max-interval".

Suggested change
"josdk.controller.ctrl.rate-limiter.limit-for-period");
"josdk.controller.ctrl.rate-limiter.limit-for-period",
"josdk.controller.ctrl.retry.max-attempts",
"josdk.controller.ctrl.retry.initial-interval",
"josdk.controller.ctrl.retry.interval-multiplier",
"josdk.controller.ctrl.retry.max-interval");

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Property/Env Configuration Adapter

4 participants