Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a24ae1d

Browse files
committedDec 8, 2022
feat: implement support for converter overriding
1 parent 36ff178 commit a24ae1d

File tree

4 files changed

+336
-24
lines changed

4 files changed

+336
-24
lines changed
 

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/Utils.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,6 @@ public static String contextFor(ControllerConfiguration<?> controllerConfigurati
237237
}
238238
context += "reconciler: " + controllerConfiguration.getName();
239239

240-
241240
return context;
242241
}
243242
}

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceConfigurationResolver.java

Lines changed: 127 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.javaoperatorsdk.operator.api.config.dependent;
22

3+
import java.lang.annotation.Annotation;
34
import java.util.HashMap;
45
import java.util.Map;
56

@@ -14,7 +15,10 @@ public class DependentResourceConfigurationResolver {
1415

1516
private DependentResourceConfigurationResolver() {}
1617

17-
private static final Map<Class, ConfigurationConverter> converters = new HashMap<>();
18+
private static final Map<Class<? extends DependentResource>, ConverterAnnotationPair> converters =
19+
new HashMap<>();
20+
private static final Map<Class<? extends ConfigurationConverter>, ConfigurationConverter> knownConverters =
21+
new HashMap<>();
1822

1923
public static <C extends ControllerConfiguration<? extends HasMetadata>> void configure(
2024
DependentResource dependentResource, DependentResourceSpec spec, C parentConfiguration) {
@@ -38,39 +42,140 @@ public static <C extends ControllerConfiguration<? extends HasMetadata>> Object
3842
}
3943

4044
// find Configured-annotated class if it exists
41-
final var dependentResourceClass = spec.getDependentResourceClass();
42-
Class<?> currentClass = dependentResourceClass;
43-
Configured configured = null;
44-
while (!Object.class.equals(currentClass)) {
45+
return extractConfigurationFromConfigured(spec.getDependentResourceClass(),
46+
parentConfiguration);
47+
}
48+
49+
public static <C extends ControllerConfiguration<? extends HasMetadata>> Object extractConfigurationFromConfigured(
50+
Class<? extends DependentResource> dependentResourceClass, C parentConfiguration) {
51+
var converterAnnotationPair = converters.get(dependentResourceClass);
52+
53+
Annotation configAnnotation;
54+
if (converterAnnotationPair == null) {
55+
var configuredClassPair = getConfigured(dependentResourceClass);
56+
if (configuredClassPair == null) {
57+
return null;
58+
}
59+
60+
// check if we already have a converter registered for the found Configured annotated class
61+
converterAnnotationPair = converters.get(configuredClassPair.annotatedClass);
62+
if (converterAnnotationPair == null) {
63+
final var configured = configuredClassPair.configured;
64+
converterAnnotationPair =
65+
getOrCreateConverter(dependentResourceClass, parentConfiguration,
66+
configured.converter(),
67+
configured.by());
68+
} else {
69+
// only register the converter pair for this dependent resource class as well
70+
converters.put(dependentResourceClass, converterAnnotationPair);
71+
}
72+
}
73+
74+
// find the associated configuration annotation
75+
configAnnotation =
76+
dependentResourceClass.getAnnotation(converterAnnotationPair.annotationClass);
77+
final var converter = converterAnnotationPair.converter;
78+
79+
// always called even if the annotation is null so that implementations can provide default
80+
// values
81+
return converter.configFrom(configAnnotation, parentConfiguration, dependentResourceClass);
82+
}
83+
84+
private static ConfiguredClassPair getConfigured(
85+
Class<? extends DependentResource> dependentResourceClass) {
86+
Class<? extends DependentResource> currentClass = dependentResourceClass;
87+
Configured configured;
88+
ConfiguredClassPair result = null;
89+
while (DependentResource.class.isAssignableFrom(currentClass)) {
4590
configured = currentClass.getAnnotation(Configured.class);
4691
if (configured != null) {
92+
result = new ConfiguredClassPair(configured, currentClass);
4793
break;
4894
}
49-
currentClass = currentClass.getSuperclass();
95+
currentClass = (Class<? extends DependentResource>) currentClass.getSuperclass();
5096
}
97+
return result;
98+
}
5199

100+
private static <C extends ControllerConfiguration<? extends HasMetadata>> ConverterAnnotationPair getOrCreateConverter(
101+
Class<? extends DependentResource> dependentResourceClass, C parentConfiguration,
102+
Class<? extends ConfigurationConverter> converterClass,
103+
Class<? extends Annotation> annotationClass) {
104+
var converterPair = converters.get(dependentResourceClass);
105+
if (converterPair == null) {
106+
// only instantiate a new converter if we haven't done so already for this converter type
107+
var converter = knownConverters.get(converterClass);
108+
if (converter == null) {
109+
converter = Utils.instantiate(converterClass,
110+
ConfigurationConverter.class,
111+
Utils.contextFor(parentConfiguration, dependentResourceClass, Configured.class));
112+
knownConverters.put(converterClass, converter);
113+
}
114+
// record dependent class - converter association for faster future retrieval
115+
converterPair = new ConverterAnnotationPair(converter, annotationClass);
116+
converters.put(dependentResourceClass, converterPair);
117+
}
118+
return converterPair;
119+
}
120+
121+
static ConfigurationConverter getConverter(
122+
Class<? extends DependentResource> dependentResourceClass) {
123+
final var converterAnnotationPair = converters.get(dependentResourceClass);
124+
return converterAnnotationPair != null ? converterAnnotationPair.converter : null;
125+
}
126+
127+
@SuppressWarnings("unused")
128+
public static void registerConverter(Class<? extends DependentResource> dependentResourceClass,
129+
ConfigurationConverter converter) {
130+
var configured = getConfigured(dependentResourceClass);
52131
if (configured == null) {
53-
return null;
132+
throw new IllegalArgumentException("There is no @" + Configured.class.getSimpleName()
133+
+ " annotation on " + dependentResourceClass.getName()
134+
+ " or its superclasses and thus doesn't need to be associated with a converter");
54135
}
136+
55137
// find the associated configuration annotation
56-
final var configAnnotation = dependentResourceClass.getAnnotation(configured.by());
57-
final var converter = getConverter(dependentResourceClass, parentConfiguration, configured);
58-
// always called even if the annotation is null so that implementations can provide default
59-
// values
60-
return converter.configFrom(configAnnotation, parentConfiguration, dependentResourceClass);
138+
final var toRegister = new ConverterAnnotationPair(converter, configured.configured.by());
139+
final Class<? extends ConfigurationConverter> converterClass = converter.getClass();
140+
converters.put(dependentResourceClass, toRegister);
141+
142+
// also register the Configured-annotated class if not the one we're registering
143+
if (!dependentResourceClass.equals(configured.annotatedClass)) {
144+
converters.put(configured.annotatedClass, toRegister);
145+
}
146+
147+
knownConverters.put(converterClass, converter);
61148
}
62149

63-
static <C extends ControllerConfiguration<? extends HasMetadata>> ConfigurationConverter getConverter(
64-
Class<? extends DependentResource> dependentResourceClass, C parentConfiguration,
65-
Configured configured) {
66-
var converter = converters.get(dependentResourceClass);
67-
if (converter == null) {
68-
converter = Utils.instantiate(configured.converter(),
69-
ConfigurationConverter.class,
70-
Utils.contextFor(parentConfiguration, dependentResourceClass, Configured.class));
71-
converters.put(dependentResourceClass, converter);
150+
private static class ConfiguredClassPair {
151+
private final Configured configured;
152+
private final Class<? extends DependentResource> annotatedClass;
153+
154+
private ConfiguredClassPair(Configured configured,
155+
Class<? extends DependentResource> annotatedClass) {
156+
this.configured = configured;
157+
this.annotatedClass = annotatedClass;
158+
}
159+
160+
@Override
161+
public String toString() {
162+
return annotatedClass.getName() + " -> " + configured;
72163
}
73-
return converter;
74164
}
75165

166+
private static class ConverterAnnotationPair {
167+
private final ConfigurationConverter converter;
168+
private final Class<? extends Annotation> annotationClass;
169+
170+
private ConverterAnnotationPair(ConfigurationConverter converter,
171+
Class<? extends Annotation> annotationClass) {
172+
this.converter = converter;
173+
this.annotationClass = annotationClass;
174+
}
175+
176+
@Override
177+
public String toString() {
178+
return converter.toString() + " -> " + annotationClass.getName();
179+
}
180+
}
76181
}

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentConverter.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
public class KubernetesDependentConverter<R extends HasMetadata, P extends HasMetadata> implements
1818
ConfigurationConverter<KubernetesDependent, KubernetesDependentResourceConfig<R>, KubernetesDependentResource<R, P>> {
1919

20-
2120
@Override
2221
@SuppressWarnings({"unchecked", "rawtypes"})
2322
public KubernetesDependentResourceConfig<R> configFrom(KubernetesDependent configAnnotation,
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
package io.javaoperatorsdk.operator.api.config.dependent;
2+
3+
import java.lang.annotation.Annotation;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.util.Optional;
7+
8+
import org.junit.jupiter.api.Test;
9+
10+
import io.fabric8.kubernetes.api.model.ConfigMap;
11+
import io.fabric8.kubernetes.api.model.Service;
12+
import io.javaoperatorsdk.operator.api.config.AnnotationControllerConfiguration;
13+
import io.javaoperatorsdk.operator.api.config.ControllerConfigurationOverrider;
14+
import io.javaoperatorsdk.operator.api.reconciler.Context;
15+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
16+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
17+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
18+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
19+
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
20+
import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult;
21+
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator;
22+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentConverter;
23+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
24+
25+
import static io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolverTest.CustomAnnotationReconciler.DR_NAME;
26+
import static org.junit.jupiter.api.Assertions.assertEquals;
27+
import static org.junit.jupiter.api.Assertions.assertNotNull;
28+
import static org.junit.jupiter.api.Assertions.assertNull;
29+
import static org.junit.jupiter.api.Assertions.assertTrue;
30+
31+
class DependentResourceConfigurationResolverTest {
32+
33+
@Test
34+
void controllerConfigurationProvidedShouldBeReturnedIfAvailable() {
35+
final var cfg = new AnnotationControllerConfiguration<>(new CustomAnnotationReconciler());
36+
final var customConfig = DependentResourceConfigurationResolver
37+
.extractConfigurationFromConfigured(CustomAnnotatedDep.class, cfg);
38+
assertTrue(customConfig instanceof CustomConfig);
39+
assertEquals(CustomAnnotatedDep.PROVIDED_VALUE, ((CustomConfig) customConfig).getValue());
40+
final var newConfig = new CustomConfig(72);
41+
final var overridden = ControllerConfigurationOverrider.override(cfg)
42+
.replacingNamedDependentResourceConfig(DR_NAME, newConfig)
43+
.build();
44+
final var spec = cfg.getDependentResources().stream()
45+
.filter(s -> DR_NAME.equals(s.getName()))
46+
.findFirst()
47+
.orElseThrow();
48+
assertEquals(newConfig,
49+
DependentResourceConfigurationResolver.configurationFor(spec, overridden));
50+
}
51+
52+
@Test
53+
void getConverterShouldWork() {
54+
final var cfg = new AnnotationControllerConfiguration<>(new CustomAnnotationReconciler());
55+
var converter = DependentResourceConfigurationResolver.getConverter(CustomAnnotatedDep.class);
56+
assertNull(converter);
57+
assertNull(DependentResourceConfigurationResolver.getConverter(ChildCustomAnnotatedDep.class));
58+
59+
// extracting configuration should trigger converter creation
60+
DependentResourceConfigurationResolver.extractConfigurationFromConfigured(
61+
CustomAnnotatedDep.class, cfg);
62+
converter = DependentResourceConfigurationResolver.getConverter(CustomAnnotatedDep.class);
63+
assertNotNull(converter);
64+
assertEquals(CustomConfigConverter.class, converter.getClass());
65+
66+
converter = DependentResourceConfigurationResolver.getConverter(ChildCustomAnnotatedDep.class);
67+
assertNull(converter);
68+
DependentResourceConfigurationResolver.extractConfigurationFromConfigured(
69+
ChildCustomAnnotatedDep.class, cfg);
70+
converter = DependentResourceConfigurationResolver.getConverter(ChildCustomAnnotatedDep.class);
71+
assertNotNull(converter);
72+
assertEquals(CustomConfigConverter.class, converter.getClass());
73+
assertEquals(DependentResourceConfigurationResolver.getConverter(CustomAnnotatedDep.class),
74+
converter);
75+
}
76+
77+
@SuppressWarnings("rawtypes")
78+
@Test
79+
void registerConverterShouldWork() {
80+
final var cfg = new AnnotationControllerConfiguration<>(new CustomAnnotationReconciler());
81+
var converter = DependentResourceConfigurationResolver.getConverter(ConfigMapDep.class);
82+
assertNull(converter);
83+
DependentResourceConfigurationResolver.extractConfigurationFromConfigured(ConfigMapDep.class,
84+
cfg);
85+
converter = DependentResourceConfigurationResolver.getConverter(ConfigMapDep.class);
86+
assertTrue(converter instanceof KubernetesDependentConverter);
87+
final var overriddenConverter = new ConfigurationConverter() {
88+
@Override
89+
public Object configFrom(Annotation configAnnotation,
90+
io.javaoperatorsdk.operator.api.config.ControllerConfiguration parentConfiguration,
91+
Class originatingClass) {
92+
return null;
93+
}
94+
};
95+
DependentResourceConfigurationResolver.registerConverter(KubernetesDependentResource.class,
96+
overriddenConverter);
97+
98+
// already resolved converters are kept unchanged
99+
converter = DependentResourceConfigurationResolver.getConverter(ConfigMapDep.class);
100+
assertTrue(converter instanceof KubernetesDependentConverter);
101+
102+
// but new converters should use the overridden version
103+
DependentResourceConfigurationResolver.extractConfigurationFromConfigured(ServiceDep.class,
104+
cfg);
105+
converter = DependentResourceConfigurationResolver.getConverter(ServiceDep.class);
106+
assertEquals(overriddenConverter, converter);
107+
}
108+
109+
@ControllerConfiguration(dependents = {
110+
@Dependent(type = CustomAnnotatedDep.class, name = DR_NAME),
111+
@Dependent(type = ChildCustomAnnotatedDep.class),
112+
@Dependent(type = ConfigMapDep.class),
113+
@Dependent(type = ServiceDep.class)
114+
})
115+
static class CustomAnnotationReconciler implements Reconciler<ConfigMap> {
116+
117+
public static final String DR_NAME = "first";
118+
119+
@Override
120+
public UpdateControl<ConfigMap> reconcile(ConfigMap resource, Context<ConfigMap> context)
121+
throws Exception {
122+
return null;
123+
}
124+
}
125+
126+
private static class ConfigMapDep extends KubernetesDependentResource<ConfigMap, ConfigMap> {
127+
128+
public ConfigMapDep() {
129+
super(ConfigMap.class);
130+
}
131+
}
132+
133+
private static class ServiceDep extends KubernetesDependentResource<Service, ConfigMap> {
134+
135+
public ServiceDep() {
136+
super(Service.class);
137+
}
138+
}
139+
140+
@CustomAnnotation(value = CustomAnnotatedDep.PROVIDED_VALUE)
141+
@Configured(by = CustomAnnotation.class, with = CustomConfig.class,
142+
converter = CustomConfigConverter.class)
143+
private static class CustomAnnotatedDep implements DependentResource<ConfigMap, ConfigMap>,
144+
DependentResourceConfigurator<CustomConfig> {
145+
146+
public static final int PROVIDED_VALUE = 42;
147+
private CustomConfig config;
148+
149+
@Override
150+
public ReconcileResult<ConfigMap> reconcile(ConfigMap primary, Context<ConfigMap> context) {
151+
return null;
152+
}
153+
154+
@Override
155+
public Class<ConfigMap> resourceType() {
156+
return ConfigMap.class;
157+
}
158+
159+
@Override
160+
public void configureWith(CustomConfig config) {
161+
this.config = config;
162+
}
163+
164+
@Override
165+
public Optional<CustomConfig> configuration() {
166+
return Optional.ofNullable(config);
167+
}
168+
}
169+
170+
private static class ChildCustomAnnotatedDep extends CustomAnnotatedDep {
171+
172+
}
173+
174+
@Retention(RetentionPolicy.RUNTIME)
175+
private @interface CustomAnnotation {
176+
177+
int value();
178+
}
179+
180+
private static class CustomConfig {
181+
182+
private final int value;
183+
184+
private CustomConfig(int value) {
185+
this.value = value;
186+
}
187+
188+
public int getValue() {
189+
return value;
190+
}
191+
}
192+
193+
private static class CustomConfigConverter
194+
implements ConfigurationConverter<CustomAnnotation, CustomConfig, CustomAnnotatedDep> {
195+
196+
static final int CONVERTER_PROVIDED_DEFAULT = 7;
197+
198+
@Override
199+
public CustomConfig configFrom(CustomAnnotation configAnnotation,
200+
io.javaoperatorsdk.operator.api.config.ControllerConfiguration<?> parentConfiguration,
201+
Class<CustomAnnotatedDep> originatingClass) {
202+
if (configAnnotation == null) {
203+
return new CustomConfig(CONVERTER_PROVIDED_DEFAULT);
204+
} else {
205+
return new CustomConfig(configAnnotation.value());
206+
}
207+
}
208+
}
209+
}

0 commit comments

Comments
 (0)
Please sign in to comment.