Skip to content

Commit 62daa4a

Browse files
csvirimetacosm
andauthored
fix: ordered managed dependents (operator-framework#1120)
Co-authored-by: Chris Laprun <metacosm@gmail.com>
1 parent ce563b7 commit 62daa4a

File tree

13 files changed

+274
-63
lines changed

13 files changed

+274
-63
lines changed

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

+12-15
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
package io.javaoperatorsdk.operator.api.config;
22

33
import java.time.Duration;
4-
import java.util.Collections;
5-
import java.util.HashMap;
6-
import java.util.Map;
7-
import java.util.Optional;
8-
import java.util.Set;
4+
import java.util.*;
95
import java.util.function.Function;
106

117
import io.fabric8.kubernetes.api.model.HasMetadata;
@@ -26,7 +22,7 @@ public class AnnotationControllerConfiguration<R extends HasMetadata>
2622

2723
protected final Reconciler<R> reconciler;
2824
private final ControllerConfiguration annotation;
29-
private Map<String, DependentResourceSpec<?, ?>> specs;
25+
private List<DependentResourceSpec<?, ?>> specs;
3026

3127
public AnnotationControllerConfiguration(Reconciler<R> reconciler) {
3228
this.reconciler = reconciler;
@@ -135,15 +131,15 @@ public static <T> T valueOrDefault(
135131

136132
@SuppressWarnings({"rawtypes", "unchecked"})
137133
@Override
138-
public Map<String, DependentResourceSpec<?, ?>> getDependentResources() {
134+
public List<DependentResourceSpec<?, ?>> getDependentResources() {
139135
if (specs == null) {
140136
final var dependents =
141137
valueOrDefault(annotation, ControllerConfiguration::dependents, new Dependent[] {});
142138
if (dependents.length == 0) {
143-
return Collections.emptyMap();
139+
return Collections.emptyList();
144140
}
145141

146-
specs = new HashMap<>(dependents.length);
142+
specs = new ArrayList<>(dependents.length);
147143
for (Dependent dependent : dependents) {
148144
Object config = null;
149145
final Class<? extends DependentResource> dependentType = dependent.type();
@@ -163,14 +159,15 @@ public static <T> T valueOrDefault(
163159
if (name.isBlank()) {
164160
name = DependentResource.defaultNameFor(dependentType);
165161
}
166-
final DependentResourceSpec<?, ?> spec = specs.get(name);
167-
if (spec != null) {
168-
throw new IllegalArgumentException(
169-
"A DependentResource named: " + name + " already exists: " + spec);
162+
for (var spec : specs) {
163+
if (spec.getName().equals(name)) {
164+
throw new IllegalArgumentException(
165+
"A DependentResource named: " + name + " already exists: " + spec);
166+
}
170167
}
171-
specs.put(name, new DependentResourceSpec(dependentType, config, name));
168+
specs.add(new DependentResourceSpec(dependentType, config, name));
172169
}
173170
}
174-
return Collections.unmodifiableMap(specs);
171+
return Collections.unmodifiableList(specs);
175172
}
176173
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import java.time.Duration;
44
import java.util.Collections;
5-
import java.util.Map;
5+
import java.util.List;
66
import java.util.Optional;
77

88
import io.fabric8.kubernetes.api.model.HasMetadata;
@@ -46,8 +46,8 @@ default ResourceEventFilter<R> getEventFilter() {
4646
return ResourceEventFilters.passthrough();
4747
}
4848

49-
default Map<String, DependentResourceSpec<?, ?>> getDependentResources() {
50-
return Collections.emptyMap();
49+
default List<DependentResourceSpec<?, ?>> getDependentResources() {
50+
return Collections.emptyList();
5151
}
5252

5353
default Optional<Duration> reconciliationMaxInterval() {

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

+13-12
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
package io.javaoperatorsdk.operator.api.config;
22

33
import java.time.Duration;
4-
import java.util.HashMap;
54
import java.util.HashSet;
5+
import java.util.LinkedHashMap;
66
import java.util.List;
7-
import java.util.Map;
87
import java.util.Set;
8+
import java.util.stream.Collectors;
99

1010
import io.fabric8.kubernetes.api.model.HasMetadata;
1111
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec;
1212
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter;
1313

14-
@SuppressWarnings({"rawtypes", "unchecked", "unused"})
14+
@SuppressWarnings({"unused"})
1515
public class ControllerConfigurationOverrider<R extends HasMetadata> {
1616

1717
private String finalizer;
@@ -22,7 +22,7 @@ public class ControllerConfigurationOverrider<R extends HasMetadata> {
2222
private ResourceEventFilter<R> customResourcePredicate;
2323
private final ControllerConfiguration<R> original;
2424
private Duration reconciliationMaxInterval;
25-
private final Map<String, DependentResourceSpec<?, ?>> dependentResourceSpecs;
25+
private final LinkedHashMap<String, DependentResourceSpec<?, ?>> namedDependentResourceSpecs;
2626

2727
private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
2828
finalizer = original.getFinalizerName();
@@ -33,7 +33,9 @@ private ControllerConfigurationOverrider(ControllerConfiguration<R> original) {
3333
customResourcePredicate = original.getEventFilter();
3434
reconciliationMaxInterval = original.reconciliationMaxInterval().orElse(null);
3535
// make the original specs modifiable
36-
dependentResourceSpecs = new HashMap<>(original.getDependentResources());
36+
final var dependentResources = original.getDependentResources();
37+
namedDependentResourceSpecs = new LinkedHashMap<>(dependentResources.size());
38+
dependentResources.forEach(drs -> namedDependentResourceSpecs.put(drs.getName(), drs));
3739
this.original = original;
3840
}
3941

@@ -92,14 +94,13 @@ public ControllerConfigurationOverrider<R> withReconciliationMaxInterval(
9294

9395
public ControllerConfigurationOverrider<R> replacingNamedDependentResourceConfig(String name,
9496
Object dependentResourceConfig) {
95-
final var currentConfig = dependentResourceSpecs.get(name);
96-
if (currentConfig == null) {
97+
98+
var namedRDS = namedDependentResourceSpecs.get(name);
99+
if (namedRDS == null) {
97100
throw new IllegalArgumentException("Cannot find a DependentResource named: " + name);
98101
}
99-
dependentResourceSpecs.remove(name);
100-
dependentResourceSpecs.put(name,
101-
new DependentResourceSpec(currentConfig.getDependentResourceClass(),
102-
dependentResourceConfig, name));
102+
namedDependentResourceSpecs.put(name, new DependentResourceSpec<>(
103+
namedRDS.getDependentResourceClass(), dependentResourceConfig, name));
103104
return this;
104105
}
105106

@@ -116,7 +117,7 @@ public ControllerConfiguration<R> build() {
116117
customResourcePredicate,
117118
original.getResourceClass(),
118119
reconciliationMaxInterval,
119-
dependentResourceSpecs);
120+
namedDependentResourceSpecs.values().stream().collect(Collectors.toUnmodifiableList()));
120121
}
121122

122123
public static <R extends HasMetadata> ControllerConfigurationOverrider<R> override(

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

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package io.javaoperatorsdk.operator.api.config;
22

33
import java.time.Duration;
4-
import java.util.Collections;
5-
import java.util.Map;
6-
import java.util.Optional;
7-
import java.util.Set;
4+
import java.util.*;
85

96
import io.fabric8.kubernetes.api.model.HasMetadata;
107
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec;
@@ -21,7 +18,7 @@ public class DefaultControllerConfiguration<R extends HasMetadata>
2118
private final boolean generationAware;
2219
private final RetryConfiguration retryConfiguration;
2320
private final ResourceEventFilter<R> resourceEventFilter;
24-
private final Map<String, DependentResourceSpec<?, ?>> dependents;
21+
private final List<DependentResourceSpec<?, ?>> dependents;
2522
private final Duration reconciliationMaxInterval;
2623

2724
// NOSONAR constructor is meant to provide all information
@@ -37,7 +34,7 @@ public DefaultControllerConfiguration(
3734
ResourceEventFilter<R> resourceEventFilter,
3835
Class<R> resourceClass,
3936
Duration reconciliationMaxInterval,
40-
Map<String, DependentResourceSpec<?, ?>> dependents) {
37+
List<DependentResourceSpec<?, ?>> dependents) {
4138
super(labelSelector, resourceClass, namespaces);
4239
this.associatedControllerClassName = associatedControllerClassName;
4340
this.name = name;
@@ -51,7 +48,7 @@ public DefaultControllerConfiguration(
5148
: retryConfiguration;
5249
this.resourceEventFilter = resourceEventFilter;
5350

54-
this.dependents = dependents != null ? dependents : Collections.emptyMap();
51+
this.dependents = dependents != null ? dependents : Collections.emptyList();
5552
}
5653

5754
@Override
@@ -90,7 +87,7 @@ public ResourceEventFilter<R> getEventFilter() {
9087
}
9188

9289
@Override
93-
public Map<String, DependentResourceSpec<?, ?>> getDependentResources() {
90+
public List<DependentResourceSpec<?, ?>> getDependentResources() {
9491
return dependents;
9592
}
9693

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

-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ public static void init() {
3434
log.debug("Initialized ExecutorServiceManager executor: {}, timeout: {}",
3535
configuration.getExecutorService().getClass(),
3636
configuration.getTerminationTimeoutSeconds());
37-
log.debug("Initialized ExecutorServiceManager executor: {}, timeout: {}",
38-
configuration.getExecutorService().getClass(),
39-
configuration.getTerminationTimeoutSeconds());
4037
} else {
4138
log.debug("Already started, reusing already setup instance!");
4239
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java

+9-14
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
package io.javaoperatorsdk.operator.processing;
22

33
import java.util.ArrayList;
4-
import java.util.Collections;
5-
import java.util.HashMap;
4+
import java.util.LinkedHashMap;
65
import java.util.Map;
76
import java.util.Optional;
87
import java.util.stream.Collectors;
@@ -54,7 +53,7 @@ public class Controller<P extends HasMetadata>
5453
private final ControllerConfiguration<P> configuration;
5554
private final KubernetesClient kubernetesClient;
5655
private final EventSourceManager<P> eventSourceManager;
57-
private final Map<String, DependentResource> dependents;
56+
private final LinkedHashMap<String, DependentResource> dependents;
5857
private final boolean contextInitializer;
5958
private final boolean hasDeleterDependents;
6059
private final boolean isCleaner;
@@ -77,18 +76,18 @@ public Controller(Reconciler<P> reconciler,
7776
final var specs = configuration.getDependentResources();
7877
final var size = specs.size();
7978
if (size == 0) {
80-
dependents = Collections.emptyMap();
79+
dependents = new LinkedHashMap<>();
8180
} else {
82-
final var dependentsHolder = new HashMap<String, DependentResource>(size);
83-
specs.forEach((name, drs) -> {
81+
final Map<String, DependentResource> dependentsHolder = new LinkedHashMap<>(size);
82+
specs.forEach(drs -> {
8483
final var dependent = createAndConfigureFrom(drs, kubernetesClient);
8584
// check if dependent implements Deleter to record that fact
8685
if (!hasDeleterHolder[0] && dependent instanceof Deleter) {
8786
hasDeleterHolder[0] = true;
8887
}
89-
dependentsHolder.put(name, dependent);
88+
dependentsHolder.put(drs.getName(), dependent);
9089
});
91-
dependents = Collections.unmodifiableMap(dependentsHolder);
90+
dependents = new LinkedHashMap<>(dependentsHolder);
9291
}
9392

9493
hasDeleterDependents = hasDeleterHolder[0];
@@ -193,7 +192,8 @@ public UpdateControl<P> execute() throws Exception {
193192
dependents.forEach((name, dependent) -> {
194193
try {
195194
final var reconcileResult = dependent.reconcile(resource, context);
196-
context.managedDependentResourceContext().setReconcileResult(name, reconcileResult);
195+
context.managedDependentResourceContext().setReconcileResult(name,
196+
reconcileResult);
197197
log.info("Reconciled dependent '{}' -> {}", name, reconcileResult.getOperation());
198198
} catch (Exception e) {
199199
final var message = e.getMessage();
@@ -377,9 +377,4 @@ public void stop() {
377377
public boolean useFinalizer() {
378378
return isCleaner || hasDeleterDependents;
379379
}
380-
381-
@SuppressWarnings("rawtypes")
382-
public Map<String, DependentResource> getDependents() {
383-
return dependents;
384-
}
385380
}

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverriderTest.java

+8-3
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@ void replaceNamedDependentResourceConfigShouldWork() {
2525
var dependents = configuration.getDependentResources();
2626
assertFalse(dependents.isEmpty());
2727
assertEquals(1, dependents.size());
28+
2829
final var dependentResourceName = DependentResource.defaultNameFor(ReadOnlyDependent.class);
29-
assertTrue(dependents.containsKey(dependentResourceName));
30-
var dependentSpec = dependents.get(dependentResourceName);
30+
assertTrue(dependents.stream().anyMatch(dr -> dr.getName().equals(dependentResourceName)));
31+
32+
var dependentSpec = dependents.stream().filter(dr -> dr.getName().equals(dependentResourceName))
33+
.findFirst().get();
3134
assertEquals(ReadOnlyDependent.class, dependentSpec.getDependentResourceClass());
3235
var maybeConfig = dependentSpec.getDependentResourceConfiguration();
3336
assertTrue(maybeConfig.isPresent());
3437
assertTrue(maybeConfig.get() instanceof KubernetesDependentResourceConfig);
38+
3539
var config = (KubernetesDependentResourceConfig) maybeConfig.orElseThrow();
3640
// check that the DependentResource inherits the controller's configuration if applicable
3741
assertEquals(1, config.namespaces().length);
@@ -47,7 +51,8 @@ void replaceNamedDependentResourceConfigShouldWork() {
4751
new KubernetesDependentResourceConfig(new String[] {overriddenNS}, labelSelector))
4852
.build();
4953
dependents = overridden.getDependentResources();
50-
dependentSpec = dependents.get(dependentResourceName);
54+
dependentSpec = dependents.stream().filter(dr -> dr.getName().equals(dependentResourceName))
55+
.findFirst().get();
5156
config = (KubernetesDependentResourceConfig) dependentSpec.getDependentResourceConfiguration()
5257
.orElseThrow();
5358
assertEquals(1, config.namespaces().length);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import java.time.Duration;
4+
5+
import org.junit.jupiter.api.Test;
6+
import org.junit.jupiter.api.extension.RegisterExtension;
7+
8+
import io.fabric8.kubernetes.api.model.ObjectMeta;
9+
import io.javaoperatorsdk.operator.junit.OperatorExtension;
10+
import io.javaoperatorsdk.operator.sample.orderedmanageddependent.ConfigMapDependentResource1;
11+
import io.javaoperatorsdk.operator.sample.orderedmanageddependent.ConfigMapDependentResource2;
12+
import io.javaoperatorsdk.operator.sample.orderedmanageddependent.OrderedManagedDependentCustomResource;
13+
import io.javaoperatorsdk.operator.sample.orderedmanageddependent.OrderedManagedDependentTestReconciler;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.awaitility.Awaitility.await;
17+
18+
class OrderedManagedDependentIT {
19+
20+
@RegisterExtension
21+
OperatorExtension operator =
22+
OperatorExtension.builder().withReconciler(new OrderedManagedDependentTestReconciler())
23+
.build();
24+
25+
@Test
26+
void managedDependentsAreReconciledInOrder() {
27+
operator.create(OrderedManagedDependentCustomResource.class, createTestResource());
28+
29+
await().atMost(Duration.ofSeconds(5))
30+
.until(() -> ((OrderedManagedDependentTestReconciler) operator.getFirstReconciler())
31+
.getNumberOfExecutions() >= 1);
32+
// todo change to more precise values when event filtering is fixed
33+
// assertThat(OrderedManagedDependentTestReconciler.dependentExecution).hasSize(4);
34+
assertThat(OrderedManagedDependentTestReconciler.dependentExecution.get(0))
35+
.isEqualTo(ConfigMapDependentResource1.class);
36+
assertThat(OrderedManagedDependentTestReconciler.dependentExecution.get(1))
37+
.isEqualTo(ConfigMapDependentResource2.class);
38+
39+
}
40+
41+
42+
private OrderedManagedDependentCustomResource createTestResource() {
43+
OrderedManagedDependentCustomResource cr = new OrderedManagedDependentCustomResource();
44+
cr.setMetadata(new ObjectMeta());
45+
cr.getMetadata().setName("test");
46+
return cr;
47+
}
48+
49+
}

0 commit comments

Comments
 (0)