Skip to content

Commit ef266b3

Browse files
committed
feat: introduce AnnotationDependentResourceConfigurator concept
The idea is to be able to configure dependents using annotations directly so that we can remove the special handling of KubernetesDependentResource from AnnotationControllerConfiguration. This results in dependent resources being instantiated and configured directly when processed from the annotations in the managed case, thus rendering the DependentResourceFactory concept obsolete. This should also lead to further simplification later.
1 parent 92ec34e commit ef266b3

File tree

17 files changed

+256
-164
lines changed

17 files changed

+256
-164
lines changed

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

Lines changed: 27 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import java.lang.annotation.Annotation;
44
import java.time.Duration;
5-
import java.util.Arrays;
65
import java.util.Collections;
76
import java.util.LinkedHashMap;
87
import java.util.List;
@@ -15,20 +14,18 @@
1514
import io.javaoperatorsdk.operator.OperatorException;
1615
import io.javaoperatorsdk.operator.ReconcilerUtils;
1716
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec;
18-
import io.javaoperatorsdk.operator.api.reconciler.*;
17+
import io.javaoperatorsdk.operator.api.reconciler.Constants;
1918
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
19+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
2020
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
2121
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
22-
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
23-
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
24-
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig;
22+
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.AnnotationDependentResourceConfigurator;
2523
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
2624
import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter;
2725
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter;
2826
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters;
2927
import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter;
3028
import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter;
31-
import io.javaoperatorsdk.operator.processing.event.source.filter.OnDeleteFilter;
3229
import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter;
3330
import io.javaoperatorsdk.operator.processing.retry.Retry;
3431

@@ -158,7 +155,7 @@ public Retry getRetry() {
158155

159156

160157
@SuppressWarnings("unchecked")
161-
private <T> void configureFromAnnotatedReconciler(T instance) {
158+
private void configureFromAnnotatedReconciler(Object instance) {
162159
if (instance instanceof AnnotationConfigurable) {
163160
AnnotationConfigurable configurable = (AnnotationConfigurable) instance;
164161
final Class<? extends Annotation> configurationClass =
@@ -171,6 +168,22 @@ private <T> void configureFromAnnotatedReconciler(T instance) {
171168
}
172169
}
173170

171+
@SuppressWarnings("unchecked")
172+
private void configureFromCustomAnnotation(Object instance) {
173+
if (instance instanceof AnnotationDependentResourceConfigurator) {
174+
AnnotationDependentResourceConfigurator configurator =
175+
(AnnotationDependentResourceConfigurator) instance;
176+
final Class<? extends Annotation> configurationClass =
177+
(Class<? extends Annotation>) Utils.getFirstTypeArgumentFromInterface(
178+
instance.getClass(), AnnotationDependentResourceConfigurator.class);
179+
final var configAnnotation = instance.getClass().getAnnotation(configurationClass);
180+
// always called even if the annotation is null so that implementations can provide default
181+
// values
182+
final var config = configurator.configFrom(configAnnotation, this);
183+
configurator.configureWith(config);
184+
}
185+
}
186+
174187
@Override
175188
@SuppressWarnings("unchecked")
176189
public Optional<OnAddFilter<P>> onAddFilter() {
@@ -208,22 +221,24 @@ public List<DependentResourceSpec> getDependentResources() {
208221

209222
final var specsMap = new LinkedHashMap<String, DependentResourceSpec>(dependents.length);
210223
for (Dependent dependent : dependents) {
211-
Object config = null;
212224
final Class<? extends DependentResource> dependentType = dependent.type();
213-
if (KubernetesDependentResource.class.isAssignableFrom(dependentType)) {
214-
config = createKubernetesResourceConfig(dependentType);
215-
}
216225

217226
final var name = getName(dependent, dependentType);
218227
var spec = specsMap.get(name);
219228
if (spec != null) {
220229
throw new IllegalArgumentException(
221230
"A DependentResource named '" + name + "' already exists: " + spec);
222231
}
232+
233+
final var dependentResource = Utils.instantiateAndConfigureIfNeeded(dependentType,
234+
DependentResource.class,
235+
Utils.contextFor(this, dependentType, Dependent.class),
236+
this::configureFromCustomAnnotation);
237+
223238
var eventSourceName = dependent.useEventSourceWithName();
224239
eventSourceName = Constants.NO_VALUE_SET.equals(eventSourceName) ? null : eventSourceName;
225240
final var context = Utils.contextFor(this, dependentType, null);
226-
spec = new DependentResourceSpec(dependentType, config, name,
241+
spec = new DependentResourceSpec(dependentResource, name,
227242
Set.of(dependent.dependsOn()),
228243
Utils.instantiate(dependent.readyPostcondition(), Condition.class, context),
229244
Utils.instantiate(dependent.reconcilePrecondition(), Condition.class, context),
@@ -245,52 +260,6 @@ private String getName(Dependent dependent, Class<? extends DependentResource> d
245260
return name;
246261
}
247262

248-
@SuppressWarnings({"rawtypes", "unchecked"})
249-
private Object createKubernetesResourceConfig(Class<? extends DependentResource> dependentType) {
250-
251-
Object config;
252-
final var kubeDependent = dependentType.getAnnotation(KubernetesDependent.class);
253-
254-
var namespaces = getNamespaces();
255-
var configuredNS = false;
256-
String labelSelector = null;
257-
OnAddFilter<? extends HasMetadata> onAddFilter = null;
258-
OnUpdateFilter<? extends HasMetadata> onUpdateFilter = null;
259-
OnDeleteFilter<? extends HasMetadata> onDeleteFilter = null;
260-
GenericFilter<? extends HasMetadata> genericFilter = null;
261-
ResourceDiscriminator<?, ? extends HasMetadata> resourceDiscriminator = null;
262-
if (kubeDependent != null) {
263-
if (!Arrays.equals(KubernetesDependent.DEFAULT_NAMESPACES,
264-
kubeDependent.namespaces())) {
265-
namespaces = Set.of(kubeDependent.namespaces());
266-
configuredNS = true;
267-
}
268-
final var fromAnnotation = kubeDependent.labelSelector();
269-
labelSelector = Constants.NO_VALUE_SET.equals(fromAnnotation) ? null : fromAnnotation;
270-
271-
final var context =
272-
Utils.contextFor(this, dependentType, null);
273-
onAddFilter = Utils.instantiate(kubeDependent.onAddFilter(), OnAddFilter.class, context);
274-
onUpdateFilter =
275-
Utils.instantiate(kubeDependent.onUpdateFilter(), OnUpdateFilter.class, context);
276-
onDeleteFilter =
277-
Utils.instantiate(kubeDependent.onDeleteFilter(), OnDeleteFilter.class, context);
278-
genericFilter =
279-
Utils.instantiate(kubeDependent.genericFilter(), GenericFilter.class, context);
280-
281-
resourceDiscriminator =
282-
Utils.instantiate(kubeDependent.resourceDiscriminator(),
283-
ResourceDiscriminator.class, context);
284-
}
285-
286-
config =
287-
new KubernetesDependentResourceConfig(namespaces, labelSelector, configuredNS,
288-
resourceDiscriminator, onAddFilter,
289-
onUpdateFilter, onDeleteFilter, genericFilter);
290-
291-
return config;
292-
}
293-
294263
public static <T> T valueOrDefault(
295264
ControllerConfiguration controllerConfiguration,
296265
Function<ControllerConfiguration, T> mapper,

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,9 @@ default ObjectMapper getObjectMapper() {
140140
return Serialization.jsonMapper();
141141
}
142142

143+
@Deprecated(forRemoval = true)
143144
default DependentResourceFactory dependentResourceFactory() {
144-
return new DependentResourceFactory() {};
145+
return null;
145146
}
146147

147148
default Optional<LeaderElectionConfiguration> getLeaderElectionConfiguration() {

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

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
import io.fabric8.kubernetes.api.model.HasMetadata;
1313
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec;
14+
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
15+
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator;
1416
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig;
1517
import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter;
1618
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter;
@@ -159,42 +161,59 @@ public ControllerConfigurationOverrider<R> withGenericFilter(GenericFilter<R> ge
159161
return this;
160162
}
161163

164+
@SuppressWarnings("unchecked")
162165
public ControllerConfigurationOverrider<R> replacingNamedDependentResourceConfig(String name,
163166
Object dependentResourceConfig) {
164167

165168
var current = namedDependentResourceSpecs.get(name);
166169
if (current == null) {
167170
throw new IllegalArgumentException("Cannot find a DependentResource named: " + name);
168171
}
169-
replaceConfig(name, dependentResourceConfig, current);
170-
return this;
171-
}
172172

173-
private void replaceConfig(String name, Object newConfig, DependentResourceSpec<?, ?> current) {
174-
namedDependentResourceSpecs.put(name,
175-
new DependentResourceSpec<>(current.getDependentResourceClass(), newConfig, name,
176-
current.getDependsOn(), current.getReadyCondition(), current.getReconcileCondition(),
177-
current.getDeletePostCondition(), current.getUseEventSourceWithName().orElse(null)));
173+
var dependentResource = current.getDependentResource();
174+
if (dependentResource instanceof DependentResourceConfigurator) {
175+
var configurator = (DependentResourceConfigurator) dependentResource;
176+
configurator.configureWith(dependentResourceConfig);
177+
}
178+
179+
return this;
178180
}
179181

180182
@SuppressWarnings("unchecked")
181183
public ControllerConfiguration<R> build() {
184+
// todo: this should be abstracted by introducing an interface to deal with listening to
185+
// namespace changes as possibly other things than the informers might be interested in reacting
186+
// to such changes
182187
// propagate namespaces if needed
183-
final List<DependentResourceSpec> newDependentSpecs;
188+
184189
final var hasModifiedNamespaces = !original.getNamespaces().equals(namespaces);
185-
newDependentSpecs = namedDependentResourceSpecs.entrySet().stream()
186-
.map(drsEntry -> {
187-
final var spec = drsEntry.getValue();
188-
189-
// if the spec has a config and it's a KubernetesDependentResourceConfig, update the
190-
// namespaces if needed, otherwise, just return the existing spec
191-
final Optional<?> maybeConfig = spec.getDependentResourceConfiguration();
192-
return maybeConfig.filter(KubernetesDependentResourceConfig.class::isInstance)
193-
.map(KubernetesDependentResourceConfig.class::cast)
194-
.filter(Predicate.not(KubernetesDependentResourceConfig::wereNamespacesConfigured))
195-
.map(c -> updateSpec(drsEntry.getKey(), spec, c))
196-
.orElse(drsEntry.getValue());
197-
}).collect(Collectors.toUnmodifiableList());
190+
final var newDependentSpecs = namedDependentResourceSpecs.values().stream()
191+
.map(spec -> {
192+
// if the dependent resource has a config and it's a KubernetesDependentResourceConfig,
193+
// update
194+
// the namespaces if needed, otherwise, do nothing
195+
DependentResource dependent = spec.getDependentResource();
196+
Optional<DependentResourceSpec> updated = Optional.empty();
197+
if (hasModifiedNamespaces && dependent instanceof DependentResourceConfigurator) {
198+
DependentResourceConfigurator configurator = (DependentResourceConfigurator) dependent;
199+
final Optional<?> config = configurator.configuration();
200+
updated = config.filter(KubernetesDependentResourceConfig.class::isInstance)
201+
.map(KubernetesDependentResourceConfig.class::cast)
202+
.filter(Predicate.not(KubernetesDependentResourceConfig::wereNamespacesConfigured))
203+
.map(c -> {
204+
// update the namespaces of the config, configure the dependent with it and update
205+
// the spec
206+
c.setNamespaces(namespaces);
207+
configurator.configureWith(c);
208+
return new DependentResourceSpec(dependent, spec.getName(), spec.getDependsOn(),
209+
spec.getReadyCondition(), spec.getReconcileCondition(),
210+
spec.getDeletePostCondition(),
211+
(String) spec.getUseEventSourceWithName().orElse(null));
212+
});
213+
}
214+
215+
return updated.orElse(spec);
216+
}).collect(Collectors.toList());
198217

199218
return new DefaultControllerConfiguration<>(
200219
original.getAssociatedReconcilerClassName(),
@@ -215,15 +234,6 @@ public ControllerConfiguration<R> build() {
215234
newDependentSpecs);
216235
}
217236

218-
@SuppressWarnings({"rawtypes", "unchecked"})
219-
private DependentResourceSpec<?, ?> updateSpec(String name, DependentResourceSpec spec,
220-
KubernetesDependentResourceConfig c) {
221-
return new DependentResourceSpec(spec.getDependentResourceClass(),
222-
c.setNamespaces(namespaces), name, spec.getDependsOn(), spec.getReadyCondition(),
223-
spec.getReconcileCondition(), spec.getDeletePostCondition(),
224-
(String) spec.getUseEventSourceWithName().orElse(null));
225-
}
226-
227237
public static <R extends HasMetadata> ControllerConfigurationOverrider<R> override(
228238
ControllerConfiguration<R> original) {
229239
return new ControllerConfigurationOverrider<>(original);

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ public static Class<?> getFirstTypeArgumentFromExtendedClass(Class<?> clazz) {
111111

112112
public static Class<?> getFirstTypeArgumentFromInterface(Class<?> clazz,
113113
Class<?> expectedImplementedInterface) {
114+
return getTypeArgumentFromInterfaceByIndex(clazz, expectedImplementedInterface, 0);
115+
}
116+
117+
public static Class<?> getTypeArgumentFromInterfaceByIndex(Class<?> clazz,
118+
Class<?> expectedImplementedInterface, int index) {
114119
if (expectedImplementedInterface.isAssignableFrom(clazz)) {
115120
final var genericInterfaces = clazz.getGenericInterfaces();
116121
Optional<? extends Class<?>> target = Optional.empty();
@@ -122,7 +127,7 @@ public static Class<?> getFirstTypeArgumentFromInterface(Class<?> clazz,
122127
.map(ParameterizedType.class::cast)
123128
.findFirst()
124129
.map(t -> {
125-
final Type argument = t.getActualTypeArguments()[0];
130+
final Type argument = t.getActualTypeArguments()[index];
126131
if (argument instanceof Class) {
127132
return (Class<?>) argument;
128133
}
@@ -148,7 +153,7 @@ public static Class<?> getFirstTypeArgumentFromInterface(Class<?> clazz,
148153
// try the parent
149154
var parent = clazz.getSuperclass();
150155
if (!Object.class.equals(parent)) {
151-
return getFirstTypeArgumentFromInterface(parent, expectedImplementedInterface);
156+
return getTypeArgumentFromInterfaceByIndex(parent, expectedImplementedInterface, index);
152157
}
153158
}
154159
throw new IllegalArgumentException("Couldn't retrieve generic parameter type from "

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

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
import java.util.Optional;
55
import java.util.Set;
66

7+
import io.fabric8.kubernetes.api.model.HasMetadata;
78
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
9+
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator;
810
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
911

10-
public class DependentResourceSpec<T extends DependentResource<?, ?>, C> {
12+
public class DependentResourceSpec<R, P extends HasMetadata, C> {
1113

12-
private final Class<T> dependentResourceClass;
13-
14-
private final C dependentResourceConfig;
14+
private final DependentResource<R, P> dependentResource;
1515

1616
private final String name;
1717

@@ -25,12 +25,11 @@ public class DependentResourceSpec<T extends DependentResource<?, ?>, C> {
2525

2626
private final String useEventSourceWithName;
2727

28-
public DependentResourceSpec(Class<T> dependentResourceClass, C dependentResourceConfig,
28+
public DependentResourceSpec(DependentResource<R, P> dependentResource,
2929
String name, Set<String> dependsOn, Condition<?, ?> readyCondition,
3030
Condition<?, ?> reconcileCondition, Condition<?, ?> deletePostCondition,
3131
String useEventSourceWithName) {
32-
this.dependentResourceClass = dependentResourceClass;
33-
this.dependentResourceConfig = dependentResourceConfig;
32+
this.dependentResource = dependentResource;
3433
this.name = name;
3534
this.dependsOn = dependsOn;
3635
this.readyCondition = readyCondition;
@@ -39,12 +38,18 @@ public DependentResourceSpec(Class<T> dependentResourceClass, C dependentResourc
3938
this.useEventSourceWithName = useEventSourceWithName;
4039
}
4140

42-
public Class<T> getDependentResourceClass() {
43-
return dependentResourceClass;
41+
@SuppressWarnings("unchecked")
42+
public Class<DependentResource<R, P>> getDependentResourceClass() {
43+
return (Class<DependentResource<R, P>>) dependentResource.getClass();
4444
}
4545

46+
@SuppressWarnings({"unchecked", "rawtypes"})
4647
public Optional<C> getDependentResourceConfiguration() {
47-
return Optional.ofNullable(dependentResourceConfig);
48+
if (dependentResource instanceof DependentResourceConfigurator) {
49+
var configurator = (DependentResourceConfigurator) dependentResource;
50+
return configurator.configuration();
51+
}
52+
return Optional.empty();
4853
}
4954

5055
public String getName() {
@@ -54,8 +59,7 @@ public String getName() {
5459
@Override
5560
public String toString() {
5661
return "DependentResourceSpec{ name='" + name +
57-
"', type=" + dependentResourceClass.getCanonicalName() +
58-
", config=" + dependentResourceConfig + '}';
62+
"', type=" + getDependentResourceClass().getCanonicalName() + '}';
5963
}
6064

6165
@Override
@@ -66,7 +70,7 @@ public boolean equals(Object o) {
6670
if (o == null || getClass() != o.getClass()) {
6771
return false;
6872
}
69-
DependentResourceSpec<?, ?> that = (DependentResourceSpec<?, ?>) o;
73+
DependentResourceSpec<?, ?, ?> that = (DependentResourceSpec<?, ?, ?>) o;
7074
return name.equals(that.name);
7175
}
7276

@@ -94,6 +98,10 @@ public Condition getDeletePostCondition() {
9498
return deletePostCondition;
9599
}
96100

101+
public DependentResource<R, P> getDependentResource() {
102+
return dependentResource;
103+
}
104+
97105
public Optional<String> getUseEventSourceWithName() {
98106
return Optional.ofNullable(useEventSourceWithName);
99107
}

0 commit comments

Comments
 (0)