Skip to content

Commit 18f41a2

Browse files
committed
Cleanup and documentation
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent 98fbd35 commit 18f41a2

File tree

6 files changed

+228
-173
lines changed

6 files changed

+228
-173
lines changed

docs/content/en/docs/documentation/configuration.md

Lines changed: 208 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,212 @@ For more information on how to use this feature, we recommend looking at how thi
149149
`KubernetesDependentResource` in the core framework, `SchemaDependentResource` in the samples or `CustomAnnotationDep`
150150
in the `BaseConfigurationServiceTest` test class.
151151

152-
## EventSource-level configuration
152+
## Loading Configuration from External Sources
153+
154+
JOSDK ships a `ConfigLoader` that bridges any key-value configuration source to the operator and
155+
controller configuration APIs. This lets you drive operator behaviour from environment variables,
156+
system properties, YAML files, or any config library (MicroProfile Config, SmallRye Config,
157+
Spring Environment, etc.) without writing glue code by hand.
158+
159+
### Architecture
160+
161+
The system is built around two thin abstractions:
162+
163+
- **[`ConfigProvider`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigProvider.java)**
164+
— a single-method interface that resolves a typed value for a dot-separated key:
165+
166+
```java
167+
public interface ConfigProvider {
168+
<T> Optional<T> getValue(String key, Class<T> type);
169+
}
170+
```
171+
172+
- **[`ConfigLoader`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java)**
173+
— reads all known JOSDK keys from a `ConfigProvider` and returns
174+
`Consumer<ConfigurationServiceOverrider>` / `Consumer<ControllerConfigurationOverrider<R>>`
175+
values that you pass directly to the `Operator` constructor or `operator.register()`.
176+
177+
The default `ConfigLoader` (no-arg constructor) stacks environment variables over system
178+
properties: environment variables win, system properties are the fallback.
179+
180+
```java
181+
// uses env vars + system properties out of the box
182+
Operator operator = new Operator(ConfigLoader.getDefault().applyConfigs());
183+
```
184+
185+
### Built-in Providers
186+
187+
| Provider | Source | Key mapping |
188+
|---|---|---|
189+
| `EnvVarConfigProvider` | `System.getenv()` | dots and hyphens → underscores, upper-cased (`josdk.check-crd``JOSDK_CHECK_CRD`) |
190+
| `PropertiesConfigProvider` | `java.util.Properties` or `.properties` file | key used as-is; use `PropertiesConfigProvider.systemProperties()` to read Java system properties |
191+
| `YamlConfigProvider` | YAML file | dot-separated key traverses nested mappings |
192+
| `AgregatePrirityListConfigProvider` | ordered list of providers | first non-empty result wins |
193+
194+
All string-based providers convert values to the target type automatically.
195+
Supported types: `String`, `Boolean`, `Integer`, `Long`, `Double`, `Duration` (ISO-8601, e.g. `PT30S`).
196+
197+
### Plugging in Any Config Library
198+
199+
`ConfigProvider` is a single-method interface, so adapting any config library takes only a few
200+
lines. As an example, here is an adapter for
201+
[SmallRye Config](https://smallrye.io/smallrye-config/):
202+
203+
```java
204+
public class SmallRyeConfigProvider implements ConfigProvider {
205+
206+
private final SmallRyeConfig config;
207+
208+
public SmallRyeConfigProvider(SmallRyeConfig config) {
209+
this.config = config;
210+
}
211+
212+
@Override
213+
public <T> Optional<T> getValue(String key, Class<T> type) {
214+
return config.getOptionalValue(key, type);
215+
}
216+
}
217+
```
218+
219+
The same pattern applies to MicroProfile Config, Spring `Environment`, Apache Commons
220+
Configuration, or any other library that can look up typed values by string key.
221+
222+
### Wiring Everything Together
223+
224+
Pass the `ConfigLoader` results when constructing the operator and registering reconcilers:
225+
226+
```java
227+
// Load operator-wide config from a YAML file via SmallRye Config
228+
URL configUrl = MyOperator.class.getResource("/application.yaml");
229+
var configLoader = new ConfigLoader(
230+
new SmallRyeConfigProvider(
231+
new SmallRyeConfigBuilder()
232+
.withSources(new YamlConfigSource(configUrl))
233+
.build()));
234+
235+
// applyConfigs() → Consumer<ConfigurationServiceOverrider>
236+
Operator operator = new Operator(configLoader.applyConfigs());
237+
238+
// applyControllerConfigs(name) → Consumer<ControllerConfigurationOverrider<R>>
239+
operator.register(new MyReconciler(),
240+
configLoader.applyControllerConfigs(MyReconciler.NAME));
241+
```
242+
243+
Only keys that are actually present in the source are applied; everything else retains its
244+
programmatic or annotation-based default.
245+
246+
You can also compose multiple sources with explicit priority using
247+
`AgregatePrirityListConfigProvider`:
248+
249+
```java
250+
var configLoader = new ConfigLoader(
251+
new AgregatePrirityListConfigProvider(List.of(
252+
new EnvVarConfigProvider(), // highest priority
253+
PropertiesConfigProvider.systemProperties(),
254+
new YamlConfigProvider(Path.of("config/operator.yaml")) // lowest priority
255+
)));
256+
```
257+
258+
### Operator-Level Configuration Keys
259+
260+
All operator-level keys are prefixed with `josdk.`.
261+
262+
#### General
263+
264+
| Key | Type | Description |
265+
|---|---|---|
266+
| `josdk.check-crd` | `Boolean` | Validate CRDs against local model on startup |
267+
| `josdk.close-client-on-stop` | `Boolean` | Close the Kubernetes client when the operator stops |
268+
| `josdk.use-ssa-to-patch-primary-resource` | `Boolean` | Use Server-Side Apply to patch the primary resource |
269+
| `josdk.clone-secondary-resources-when-getting-from-cache` | `Boolean` | Clone secondary resources on cache reads |
270+
271+
#### Reconciliation
272+
273+
| Key | Type | Description |
274+
|---|---|---|
275+
| `josdk.reconciliation.concurrent-threads` | `Integer` | Thread pool size for reconciliation |
276+
| `josdk.reconciliation.termination-timeout` | `Duration` | How long to wait for in-flight reconciliations to finish on shutdown |
277+
278+
#### Workflow
279+
280+
| Key | Type | Description |
281+
|---|---|---|
282+
| `josdk.workflow.executor-threads` | `Integer` | Thread pool size for workflow execution |
283+
284+
#### Informer
285+
286+
| Key | Type | Description |
287+
|---|---|---|
288+
| `josdk.informer.cache-sync-timeout` | `Duration` | Timeout for the initial informer cache sync |
289+
| `josdk.informer.stop-on-error-during-startup` | `Boolean` | Stop the operator if an informer fails to start |
290+
291+
#### Dependent Resources
292+
293+
| Key | Type | Description |
294+
|---|---|---|
295+
| `josdk.dependent-resources.ssa-based-create-update-match` | `Boolean` | Use SSA-based matching for dependent resource create/update |
296+
297+
#### Leader Election
298+
299+
Leader election is activated when at least one `josdk.leader-election.*` key is present.
300+
`josdk.leader-election.lease-name` is required when any other leader-election key is set.
301+
Setting `josdk.leader-election.enabled=false` suppresses leader election even if other keys are
302+
present.
303+
304+
| Key | Type | Description |
305+
|---|---|---|
306+
| `josdk.leader-election.enabled` | `Boolean` | Explicitly enable (`true`) or disable (`false`) leader election |
307+
| `josdk.leader-election.lease-name` | `String` | **Required.** Name of the Kubernetes Lease object used for leader election |
308+
| `josdk.leader-election.lease-namespace` | `String` | Namespace for the Lease object (defaults to the operator's namespace) |
309+
| `josdk.leader-election.identity` | `String` | Unique identity for this instance; defaults to the pod name |
310+
| `josdk.leader-election.lease-duration` | `Duration` | How long a lease is valid (default `PT15S`) |
311+
| `josdk.leader-election.renew-deadline` | `Duration` | How long the leader tries to renew before giving up (default `PT10S`) |
312+
| `josdk.leader-election.retry-period` | `Duration` | How often a candidate polls while waiting to become leader (default `PT2S`) |
313+
314+
### Controller-Level Configuration Keys
315+
316+
All controller-level keys are prefixed with `josdk.controller.<controller-name>.`, where
317+
`<controller-name>` is the value returned by the reconciler's name (typically set via
318+
`@ControllerConfiguration(name = "...")`).
319+
320+
#### General
321+
322+
| Key | Type | Description |
323+
|---|---|---|
324+
| `josdk.controller.<name>.finalizer` | `String` | Finalizer string added to managed resources |
325+
| `josdk.controller.<name>.generation-aware` | `Boolean` | Skip reconciliation when the resource generation has not changed |
326+
| `josdk.controller.<name>.label-selector` | `String` | Label selector to filter watched resources |
327+
| `josdk.controller.<name>.max-reconciliation-interval` | `Duration` | Maximum interval between reconciliations even without events |
328+
| `josdk.controller.<name>.field-manager` | `String` | Field manager name used for SSA operations |
329+
| `josdk.controller.<name>.trigger-reconciler-on-all-events` | `Boolean` | Trigger reconciliation on every event, not only meaningful changes |
330+
331+
#### Informer
332+
333+
| Key | Type | Description |
334+
|---|---|---|
335+
| `josdk.controller.<name>.informer.label-selector` | `String` | Label selector for the primary resource informer (alias for `label-selector`) |
336+
| `josdk.controller.<name>.informer.list-limit` | `Long` | Page size for paginated informer list requests; omit for no pagination |
337+
338+
#### Retry
339+
340+
If any `retry.*` key is present, a `GenericRetry` is configured starting from the
341+
[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).
342+
Only explicitly set keys override the defaults.
343+
344+
| Key | Type | Description |
345+
|---|---|---|
346+
| `josdk.controller.<name>.retry.max-attempts` | `Integer` | Maximum number of retry attempts |
347+
| `josdk.controller.<name>.retry.initial-interval` | `Long` (ms) | Initial backoff interval in milliseconds |
348+
| `josdk.controller.<name>.retry.interval-multiplier` | `Double` | Exponential backoff multiplier |
349+
| `josdk.controller.<name>.retry.max-interval` | `Long` (ms) | Maximum backoff interval in milliseconds |
350+
351+
#### Rate Limiter
352+
353+
The rate limiter is only activated when `rate-limiter.limit-for-period` is present and has a
354+
positive value. `rate-limiter.refresh-period` is optional and falls back to the default of 10 s.
355+
356+
| Key | Type | Description |
357+
|---|---|---|
358+
| `josdk.controller.<name>.rate-limiter.limit-for-period` | `Integer` | Maximum number of reconciliations allowed per refresh period. Must be positive to activate the limiter |
359+
| `josdk.controller.<name>.rate-limiter.refresh-period` | `Duration` | Window over which the limit is counted (default `PT10S`) |
153360

154-
TODO

operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/ConfigLoader.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import io.javaoperatorsdk.operator.api.config.LeaderElectionConfigurationBuilder;
3030
import io.javaoperatorsdk.operator.config.loader.provider.AgregatePrirityListConfigProvider;
3131
import io.javaoperatorsdk.operator.config.loader.provider.EnvVarConfigProvider;
32-
import io.javaoperatorsdk.operator.config.loader.provider.SystemPropertyConfigProvider;
32+
import io.javaoperatorsdk.operator.config.loader.provider.PropertiesConfigProvider;
3333
import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter;
3434
import io.javaoperatorsdk.operator.processing.retry.GenericRetry;
3535

@@ -167,7 +167,7 @@ public static ConfigLoader getDefault() {
167167
public ConfigLoader() {
168168
this(
169169
new AgregatePrirityListConfigProvider(
170-
List.of(new EnvVarConfigProvider(), new SystemPropertyConfigProvider())),
170+
List.of(new EnvVarConfigProvider(), PropertiesConfigProvider.systemProperties())),
171171
DEFAULT_CONTROLLER_KEY_PREFIX,
172172
DEFAULT_OPERATOR_KEY_PREFIX);
173173
}

operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/PropertiesConfigProvider.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public class PropertiesConfigProvider implements ConfigProvider {
3636

3737
private final Properties properties;
3838

39+
/** Returns a {@link PropertiesConfigProvider} backed by {@link System#getProperties()}. */
40+
public static PropertiesConfigProvider systemProperties() {
41+
return new PropertiesConfigProvider(System.getProperties());
42+
}
43+
3944
/**
4045
* Loads properties from the given file path.
4146
*

operator-framework/src/main/java/io/javaoperatorsdk/operator/config/loader/provider/SystemPropertyConfigProvider.java

Lines changed: 0 additions & 53 deletions
This file was deleted.

operator-framework/src/test/java/io/javaoperatorsdk/operator/config/loader/provider/PriorityListConfigProviderTest.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,27 @@
1616
package io.javaoperatorsdk.operator.config.loader.provider;
1717

1818
import java.util.List;
19+
import java.util.Properties;
1920

2021
import org.junit.jupiter.api.Test;
2122

2223
import static org.assertj.core.api.Assertions.assertThat;
2324

2425
class PriorityListConfigProviderTest {
2526

27+
private static PropertiesConfigProvider propsProvider(String key, String value) {
28+
Properties props = new Properties();
29+
if (key != null) {
30+
props.setProperty(key, value);
31+
}
32+
return new PropertiesConfigProvider(props);
33+
}
34+
2635
@Test
2736
void returnsEmptyWhenAllProvidersReturnEmpty() {
2837
var provider =
2938
new AgregatePrirityListConfigProvider(
30-
List.of(
31-
new EnvVarConfigProvider(k -> null), new SystemPropertyConfigProvider(k -> null)));
39+
List.of(new EnvVarConfigProvider(k -> null), propsProvider(null, null)));
3240
assertThat(provider.getValue("josdk.no.such.key", String.class)).isEmpty();
3341
}
3442

@@ -38,8 +46,7 @@ void firstProviderWins() {
3846
new AgregatePrirityListConfigProvider(
3947
List.of(
4048
new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_KEY") ? "first" : null),
41-
new SystemPropertyConfigProvider(
42-
k -> k.equals("josdk.test.key") ? "second" : null)));
49+
propsProvider("josdk.test.key", "second")));
4350
assertThat(provider.getValue("josdk.test.key", String.class)).hasValue("first");
4451
}
4552

@@ -49,16 +56,14 @@ void fallsBackToLaterProviderWhenEarlierReturnsEmpty() {
4956
new AgregatePrirityListConfigProvider(
5057
List.of(
5158
new EnvVarConfigProvider(k -> null),
52-
new SystemPropertyConfigProvider(
53-
k -> k.equals("josdk.test.key") ? "from-second" : null)));
59+
propsProvider("josdk.test.key", "from-second")));
5460
assertThat(provider.getValue("josdk.test.key", String.class)).hasValue("from-second");
5561
}
5662

5763
@Test
5864
void respectsOrderWithThreeProviders() {
5965
var first = new EnvVarConfigProvider(k -> null);
60-
var second =
61-
new SystemPropertyConfigProvider(k -> k.equals("josdk.test.key") ? "from-second" : null);
66+
var second = propsProvider("josdk.test.key", "from-second");
6267
var third = new EnvVarConfigProvider(k -> k.equals("JOSDK_TEST_KEY") ? "from-third" : null);
6368

6469
var provider = new AgregatePrirityListConfigProvider(List.of(first, second, third));

0 commit comments

Comments
 (0)