Skip to content

Commit 623b510

Browse files
authored
Merge pull request #306 from java-operator-sdk/quarkus-external-config
feat(quarkus): add support for externalized configuration
2 parents ee18088 + 22ccebc commit 623b510

File tree

12 files changed

+384
-49
lines changed

12 files changed

+384
-49
lines changed

operator-framework-quarkus-extension/deployment/src/main/java/io/javaoperatorsdk/quarkus/extension/deployment/QuarkusExtensionProcessor.java

Lines changed: 87 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import io.javaoperatorsdk.operator.api.ResourceController;
77
import io.javaoperatorsdk.operator.api.config.ConfigurationService;
88
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
9+
import io.javaoperatorsdk.operator.api.config.RetryConfiguration;
910
import io.javaoperatorsdk.quarkus.extension.ConfigurationServiceRecorder;
11+
import io.javaoperatorsdk.quarkus.extension.ExternalConfiguration;
12+
import io.javaoperatorsdk.quarkus.extension.ExternalControllerConfiguration;
1013
import io.javaoperatorsdk.quarkus.extension.OperatorProducer;
1114
import io.javaoperatorsdk.quarkus.extension.QuarkusConfigurationService;
1215
import io.javaoperatorsdk.quarkus.extension.QuarkusControllerConfiguration;
@@ -49,6 +52,8 @@ class QuarkusExtensionProcessor {
4952
throw new IllegalArgumentException();
5053
};
5154

55+
private ExternalConfiguration externalConfiguration;
56+
5257
@BuildStep
5358
void indexSDKDependencies(
5459
BuildProducer<IndexDependencyBuildItem> indexDependency,
@@ -110,16 +115,6 @@ private ControllerConfiguration createControllerConfiguration(
110115
.setDefaultScope(APPLICATION_SCOPED)
111116
.build());
112117

113-
// generate configuration
114-
final var controllerAnnotation = info.classAnnotation(CONTROLLER);
115-
if (controllerAnnotation == null) {
116-
throw new IllegalArgumentException(
117-
resourceControllerClassName
118-
+ " is missing the @"
119-
+ Controller.class.getCanonicalName()
120-
+ " annotation");
121-
}
122-
123118
// load CR class
124119
final Class<CustomResource> crClass = (Class<CustomResource>) loadClass(crType);
125120

@@ -137,38 +132,45 @@ private ControllerConfiguration createControllerConfiguration(
137132
// register CR class for introspection
138133
reflectionClasses.produce(new ReflectiveClassBuildItem(true, true, crClass));
139134

135+
// retrieve the Controller annotation if it exists
136+
final var controllerAnnotation = info.classAnnotation(CONTROLLER);
137+
138+
// retrieve the controller's name
139+
final var defaultControllerName =
140+
ControllerUtils.getDefaultResourceControllerName(resourceControllerClassName);
140141
final var name =
141-
valueOrDefault(
142-
controllerAnnotation,
143-
"name",
144-
AnnotationValue::asString,
145-
() -> ControllerUtils.getDefaultResourceControllerName(resourceControllerClassName));
142+
annotationValueOrDefault(
143+
controllerAnnotation, "name", AnnotationValue::asString, () -> defaultControllerName);
144+
145+
// check if we have externalized configuration to provide values
146+
final var extContConfig = externalConfiguration.controllers.get(name);
147+
148+
final var extractor = new ValueExtractor(controllerAnnotation, extContConfig);
146149

147150
// create the configuration
148151
final var configuration =
149152
new QuarkusControllerConfiguration(
150153
resourceControllerClassName,
151154
name,
152155
crdName,
153-
valueOrDefault(
154-
controllerAnnotation,
156+
extractor.extract(
157+
c -> c.finalizer,
155158
"finalizerName",
156159
AnnotationValue::asString,
157160
() -> ControllerUtils.getDefaultFinalizerName(crdName)),
158-
valueOrDefault(
159-
controllerAnnotation,
161+
extractor.extract(
162+
c -> c.generationAware,
160163
"generationAwareEventProcessing",
161164
AnnotationValue::asBoolean,
162165
() -> true),
163166
QuarkusControllerConfiguration.asSet(
164-
valueOrDefault(
165-
controllerAnnotation,
167+
extractor.extract(
168+
c -> c.namespaces.map(l -> l.toArray(new String[0])),
166169
"namespaces",
167170
AnnotationValue::asStringArray,
168171
() -> new String[] {})),
169172
crType,
170-
null // todo: fix-me
171-
);
173+
retryConfiguration(extContConfig));
172174

173175
log.infov(
174176
"Processed ''{0}'' controller named ''{1}'' for ''{2}'' CR (version ''{3}'')",
@@ -177,12 +179,72 @@ private ControllerConfiguration createControllerConfiguration(
177179
return configuration;
178180
}
179181

180-
private <T> T valueOrDefault(
182+
private RetryConfiguration retryConfiguration(ExternalControllerConfiguration extConfig) {
183+
return extConfig == null ? null : RetryConfigurationResolver.resolve(extConfig.retry);
184+
}
185+
186+
private static class ValueExtractor {
187+
188+
private final AnnotationInstance controllerAnnotation;
189+
private final ExternalControllerConfiguration extContConfig;
190+
191+
ValueExtractor(
192+
AnnotationInstance controllerAnnotation, ExternalControllerConfiguration extContConfig) {
193+
this.controllerAnnotation = controllerAnnotation;
194+
this.extContConfig = extContConfig;
195+
}
196+
197+
/**
198+
* Extracts the appropriate configuration value for the controller checking first any annotation
199+
* configuration, then potentially overriding it by a properties-provided value or returning a
200+
* default value if neither is provided.
201+
*
202+
* @param extractor a Function extracting the optional value we're interested in from the
203+
* external configuration
204+
* @param annotationField the name of the {@link Controller} annotation we're want to retrieve
205+
* if present
206+
* @param converter a Function converting the annotation value to the type we're expecting
207+
* @param defaultValue a Supplier that computes/retrieve a default value when needed
208+
* @param <T> the expected type of the configuration value we're trying to extract
209+
* @return the extracted configuration value
210+
*/
211+
<T> T extract(
212+
Function<ExternalControllerConfiguration, Optional<T>> extractor,
213+
String annotationField,
214+
Function<AnnotationValue, T> converter,
215+
Supplier<T> defaultValue) {
216+
// first check if we have an external configuration
217+
if (extContConfig != null) {
218+
// extract value from config if present
219+
return extractor
220+
.apply(extContConfig)
221+
// or get from the annotation or default
222+
.orElse(annotationValueOrDefault(annotationField, converter, defaultValue));
223+
} else {
224+
// get from annotation or default
225+
return annotationValueOrDefault(annotationField, converter, defaultValue);
226+
}
227+
}
228+
229+
private <T> T annotationValueOrDefault(
230+
String name, Function<AnnotationValue, T> converter, Supplier<T> defaultValue) {
231+
return QuarkusExtensionProcessor.annotationValueOrDefault(
232+
controllerAnnotation, name, converter, defaultValue);
233+
}
234+
}
235+
236+
private static <T> T annotationValueOrDefault(
181237
AnnotationInstance annotation,
182238
String name,
183239
Function<AnnotationValue, T> converter,
184240
Supplier<T> defaultValue) {
185-
return Optional.ofNullable(annotation.value(name)).map(converter).orElseGet(defaultValue);
241+
return annotation != null
242+
?
243+
// get converted annotation value of get default
244+
Optional.ofNullable(annotation.value(name)).map(converter).orElseGet(defaultValue)
245+
:
246+
// get default
247+
defaultValue.get();
186248
}
187249

188250
private Class<?> loadClass(String className) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package io.javaoperatorsdk.quarkus.extension.deployment;
2+
3+
import io.javaoperatorsdk.operator.api.config.RetryConfiguration;
4+
import io.javaoperatorsdk.quarkus.extension.ExternalIntervalConfiguration;
5+
import io.javaoperatorsdk.quarkus.extension.ExternalRetryConfiguration;
6+
import io.javaoperatorsdk.quarkus.extension.PlainRetryConfiguration;
7+
import java.util.Optional;
8+
9+
class RetryConfigurationResolver implements RetryConfiguration {
10+
11+
private final RetryConfiguration delegate;
12+
13+
private RetryConfigurationResolver(Optional<ExternalRetryConfiguration> retry) {
14+
delegate =
15+
retry
16+
.<RetryConfiguration>map(ExternalRetryConfigurationAdapter::new)
17+
.orElse(RetryConfiguration.DEFAULT);
18+
}
19+
20+
public static RetryConfiguration resolve(Optional<ExternalRetryConfiguration> retry) {
21+
final var delegate = new RetryConfigurationResolver(retry);
22+
return new PlainRetryConfiguration(
23+
delegate.getMaxAttempts(),
24+
delegate.getInitialInterval(),
25+
delegate.getIntervalMultiplier(),
26+
delegate.getMaxInterval());
27+
}
28+
29+
@Override
30+
public int getMaxAttempts() {
31+
return delegate.getMaxAttempts();
32+
}
33+
34+
@Override
35+
public long getInitialInterval() {
36+
return delegate.getInitialInterval();
37+
}
38+
39+
@Override
40+
public double getIntervalMultiplier() {
41+
return delegate.getIntervalMultiplier();
42+
}
43+
44+
@Override
45+
public long getMaxInterval() {
46+
return delegate.getMaxInterval();
47+
}
48+
49+
private static class ExternalRetryConfigurationAdapter implements RetryConfiguration {
50+
51+
private final int maxAttempts;
52+
private final IntervalConfigurationAdapter interval;
53+
54+
public ExternalRetryConfigurationAdapter(ExternalRetryConfiguration config) {
55+
maxAttempts = config.maxAttempts.orElse(RetryConfiguration.DEFAULT.getMaxAttempts());
56+
interval =
57+
config
58+
.interval
59+
.map(IntervalConfigurationAdapter::new)
60+
.orElse(new IntervalConfigurationAdapter());
61+
}
62+
63+
@Override
64+
public int getMaxAttempts() {
65+
return maxAttempts;
66+
}
67+
68+
@Override
69+
public long getInitialInterval() {
70+
return interval.initial;
71+
}
72+
73+
@Override
74+
public double getIntervalMultiplier() {
75+
return interval.multiplier;
76+
}
77+
78+
@Override
79+
public long getMaxInterval() {
80+
return interval.max;
81+
}
82+
}
83+
84+
private static class IntervalConfigurationAdapter {
85+
86+
private final long initial;
87+
private final double multiplier;
88+
private final long max;
89+
90+
IntervalConfigurationAdapter(ExternalIntervalConfiguration config) {
91+
initial = config.initial.orElse(RetryConfiguration.DEFAULT.getInitialInterval());
92+
multiplier = config.multiplier.orElse(RetryConfiguration.DEFAULT.getIntervalMultiplier());
93+
max = config.max.orElse(RetryConfiguration.DEFAULT.getMaxInterval());
94+
}
95+
96+
IntervalConfigurationAdapter() {
97+
this.initial = RetryConfiguration.DEFAULT.getInitialInterval();
98+
this.multiplier = RetryConfiguration.DEFAULT.getIntervalMultiplier();
99+
this.max = RetryConfiguration.DEFAULT.getMaxInterval();
100+
}
101+
}
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.javaoperatorsdk.quarkus.extension;
2+
3+
import io.quarkus.runtime.annotations.ConfigItem;
4+
import io.quarkus.runtime.annotations.ConfigPhase;
5+
import io.quarkus.runtime.annotations.ConfigRoot;
6+
import java.util.Map;
7+
8+
@ConfigRoot(name = "operator-sdk", phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED)
9+
public class ExternalConfiguration {
10+
11+
/** Maps a controller name to its configuration. */
12+
@ConfigItem public Map<String, ExternalControllerConfiguration> controllers;
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.javaoperatorsdk.quarkus.extension;
2+
3+
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
4+
import io.quarkus.runtime.annotations.ConfigGroup;
5+
import io.quarkus.runtime.annotations.ConfigItem;
6+
import java.util.List;
7+
import java.util.Optional;
8+
9+
@ConfigGroup
10+
public class ExternalControllerConfiguration {
11+
12+
/**
13+
* An optional list of comma-separated namespace names the controller should watch. If the list
14+
* contains {@link ControllerConfiguration#WATCH_ALL_NAMESPACES_MARKER} then the controller will
15+
* watch all namespaces.
16+
*/
17+
@ConfigItem public Optional<List<String>> namespaces;
18+
19+
/**
20+
* The optional name of the finalizer for the controller. If none is provided, one will be
21+
* automatically generated.
22+
*/
23+
@ConfigItem public Optional<String> finalizer;
24+
25+
/**
26+
* Whether the controller should only process events if the associated resource generation has
27+
* increased since last reconciliation, otherwise will process all events.
28+
*/
29+
@ConfigItem(defaultValue = "true")
30+
public Optional<Boolean> generationAware;
31+
32+
/** The optional controller retry configuration */
33+
public Optional<ExternalRetryConfiguration> retry;
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.javaoperatorsdk.quarkus.extension;
2+
3+
import io.quarkus.runtime.annotations.ConfigGroup;
4+
import io.quarkus.runtime.annotations.ConfigItem;
5+
import java.util.Optional;
6+
7+
@ConfigGroup
8+
public class ExternalIntervalConfiguration {
9+
10+
/** The initial interval that the controller waits for before attempting the first retry */
11+
@ConfigItem public Optional<Long> initial;
12+
13+
/** The value by which the initial interval is multiplied by for each retry */
14+
@ConfigItem public Optional<Double> multiplier;
15+
16+
/**
17+
* The maximum interval that the controller will wait for before attempting a retry, regardless of
18+
* all other configuration
19+
*/
20+
@ConfigItem public Optional<Long> max;
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.javaoperatorsdk.quarkus.extension;
2+
3+
import io.quarkus.runtime.annotations.ConfigGroup;
4+
import io.quarkus.runtime.annotations.ConfigItem;
5+
import java.util.Optional;
6+
7+
@ConfigGroup
8+
public class ExternalRetryConfiguration {
9+
10+
/** How many times an operation should be retried before giving up */
11+
@ConfigItem public Optional<Integer> maxAttempts;
12+
13+
/** The configuration of the retry interval. */
14+
@ConfigItem public Optional<ExternalIntervalConfiguration> interval;
15+
}

0 commit comments

Comments
 (0)