Skip to content

Commit d24d4ca

Browse files
csvirimetacosm
authored andcommitted
feat: distinguish resources based on desired state (#2252)
Signed-off-by: Attila Mészáros <[email protected]> Signed-off-by: Attila Mészáros <[email protected]>
1 parent 0a92cc5 commit d24d4ca

File tree

28 files changed

+643
-90
lines changed

28 files changed

+643
-90
lines changed

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/DependentResource.java

+10
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ default Optional<? extends ResourceEventSource<R, P>> eventSource(
4949
return Optional.empty();
5050
}
5151

52+
/**
53+
* Retrieves the secondary resource (if it exists) associated with the specified primary resource
54+
* for this DependentResource.
55+
*
56+
* @param primary the primary resource for which we want to retrieve the secondary resource
57+
* associated with this DependentResource
58+
* @param context the current {@link Context} in which the operation is called
59+
* @return the secondary resource or {@link Optional#empty()} if it doesn't exist
60+
* @throws IllegalStateException if more than one secondary is found to match the primary resource
61+
*/
5262
default Optional<R> getSecondaryResource(P primary, Context<P> context) {
5363
return Optional.empty();
5464
}

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java

+35-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.javaoperatorsdk.operator.processing.dependent;
22

33
import java.util.Optional;
4+
import java.util.Set;
45

56
import org.slf4j.Logger;
67
import org.slf4j.LoggerFactory;
@@ -104,8 +105,39 @@ protected ReconcileResult<R> reconcile(P primary, R actualResource, Context<P> c
104105

105106
@Override
106107
public Optional<R> getSecondaryResource(P primary, Context<P> context) {
107-
return resourceDiscriminator == null ? context.getSecondaryResource(resourceType())
108-
: resourceDiscriminator.distinguish(resourceType(), primary, context);
108+
if (resourceDiscriminator != null) {
109+
return resourceDiscriminator.distinguish(resourceType(), primary, context);
110+
} else {
111+
var secondaryResources = context.getSecondaryResources(resourceType());
112+
if (secondaryResources.isEmpty()) {
113+
return Optional.empty();
114+
} else {
115+
return selectManagedSecondaryResource(secondaryResources, primary, context);
116+
}
117+
}
118+
}
119+
120+
/**
121+
* Selects the actual secondary resource matching the desired state derived from the primary
122+
* resource when several resources of the same type are found in the context. This method allows
123+
* for optimized implementations in subclasses since this default implementation will check each
124+
* secondary candidates for equality with the specified desired state, which might end up costly.
125+
*
126+
* @param secondaryResources to select the target resource from
127+
*
128+
* @return the matching secondary resource or {@link Optional#empty()} if none matches
129+
* @throws IllegalStateException if more than one candidate is found, in which case some other
130+
* mechanism might be necessary to distinguish between candidate secondary resources
131+
*/
132+
protected Optional<R> selectManagedSecondaryResource(Set<R> secondaryResources, P primary,
133+
Context<P> context) {
134+
R desired = desired(primary, context);
135+
var targetResources = secondaryResources.stream().filter(r -> r.equals(desired)).toList();
136+
if (targetResources.size() > 1) {
137+
throw new IllegalStateException(
138+
"More than one secondary resource related to primary: " + targetResources);
139+
}
140+
return targetResources.isEmpty() ? Optional.empty() : Optional.of(targetResources.get(0));
109141
}
110142

111143
private void throwIfNull(R desired, P primary, String descriptor) {
@@ -173,8 +205,7 @@ protected void handleDelete(P primary, R secondary, Context<P> context) {
173205
"handleDelete method must be implemented if Deleter trait is supported");
174206
}
175207

176-
public void setResourceDiscriminator(
177-
ResourceDiscriminator<R, P> resourceDiscriminator) {
208+
public void setResourceDiscriminator(ResourceDiscriminator<R, P> resourceDiscriminator) {
178209
this.resourceDiscriminator = resourceDiscriminator;
179210
}
180211

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java

+24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
22

33
import java.util.Map;
4+
import java.util.Objects;
45
import java.util.Optional;
56
import java.util.Set;
67

@@ -285,6 +286,29 @@ protected void addSecondaryToPrimaryMapperAnnotations(R desired, P primary, Stri
285286
}
286287
}
287288

289+
@Override
290+
protected Optional<R> selectManagedSecondaryResource(Set<R> secondaryResources, P primary,
291+
Context<P> context) {
292+
ResourceID managedResourceID = managedSecondaryResourceID(primary, context);
293+
return secondaryResources.stream()
294+
.filter(r -> r.getMetadata().getName().equals(managedResourceID.getName()) &&
295+
Objects.equals(r.getMetadata().getNamespace(),
296+
managedResourceID.getNamespace().orElse(null)))
297+
.findFirst();
298+
}
299+
300+
/**
301+
* Override this method in order to optimize and not compute the desired when selecting the target
302+
* secondary resource. Simply, a static ResourceID can be returned.
303+
*
304+
* @param primary resource
305+
* @param context of current reconciliation
306+
* @return id of the target managed resource
307+
*/
308+
protected ResourceID managedSecondaryResourceID(P primary, Context<P> context) {
309+
return ResourceID.fromResource(desired(primary, context));
310+
}
311+
288312
protected boolean addOwnerReference() {
289313
return garbageCollected;
290314
}

Diff for: operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateBulkIT.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class ExternalStateBulkIT {
3434
.build();
3535

3636
@Test
37-
void reconcilesResourceWithPersistentState() throws InterruptedException {
37+
void reconcilesResourceWithPersistentState() {
3838
var resource = operator.create(testResource());
3939
assertResources(resource, INITIAL_TEST_DATA, INITIAL_BULK_SIZE);
4040

Original file line numberDiff line numberDiff line change
@@ -1,62 +1,80 @@
11
package io.javaoperatorsdk.operator;
22

33
import java.time.Duration;
4-
import java.util.stream.IntStream;
54

65
import org.junit.jupiter.api.Test;
76
import org.junit.jupiter.api.extension.RegisterExtension;
87

98
import io.fabric8.kubernetes.api.model.ConfigMap;
109
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
1110
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
12-
import io.javaoperatorsdk.operator.sample.multipledependentresource.MultipleDependentResourceConfigMap;
1311
import io.javaoperatorsdk.operator.sample.multipledependentresource.MultipleDependentResourceCustomResource;
1412
import io.javaoperatorsdk.operator.sample.multipledependentresource.MultipleDependentResourceReconciler;
13+
import io.javaoperatorsdk.operator.sample.multipledependentresource.MultipleDependentResourceSpec;
14+
import io.javaoperatorsdk.operator.sample.multipledrsametypenodiscriminator.*;
1515

16+
import static io.javaoperatorsdk.operator.sample.multipledependentresource.MultipleDependentResourceConfigMap.DATA_KEY;
17+
import static io.javaoperatorsdk.operator.sample.multipledependentresource.MultipleDependentResourceConfigMap.getConfigMapName;
18+
import static io.javaoperatorsdk.operator.sample.multipledependentresource.MultipleDependentResourceReconciler.FIRST_CONFIG_MAP_ID;
19+
import static io.javaoperatorsdk.operator.sample.multipledependentresource.MultipleDependentResourceReconciler.SECOND_CONFIG_MAP_ID;
1620
import static org.assertj.core.api.Assertions.assertThat;
1721
import static org.awaitility.Awaitility.await;
1822

19-
class MultipleDependentResourceIT {
23+
public class MultipleDependentResourceIT {
24+
25+
public static final String CHANGED_VALUE = "changed value";
26+
public static final String INITIAL_VALUE = "initial value";
2027

21-
public static final String TEST_RESOURCE_NAME = "multipledependentresource-testresource";
2228
@RegisterExtension
23-
LocallyRunOperatorExtension operator =
29+
LocallyRunOperatorExtension extension =
2430
LocallyRunOperatorExtension.builder()
25-
.withReconciler(MultipleDependentResourceReconciler.class)
26-
.waitForNamespaceDeletion(true)
31+
.withReconciler(new MultipleDependentResourceReconciler())
2732
.build();
2833

2934
@Test
30-
void twoConfigMapsHaveBeenCreated() {
31-
MultipleDependentResourceCustomResource customResource = createTestCustomResource();
32-
operator.create(customResource);
33-
34-
var reconciler = operator.getReconcilerOfType(MultipleDependentResourceReconciler.class);
35-
36-
await().pollDelay(Duration.ofMillis(300))
37-
.until(() -> reconciler.getNumberOfExecutions() <= 1);
38-
39-
IntStream.of(MultipleDependentResourceReconciler.FIRST_CONFIG_MAP_ID,
40-
MultipleDependentResourceReconciler.SECOND_CONFIG_MAP_ID).forEach(configMapId -> {
41-
ConfigMap configMap =
42-
operator.get(ConfigMap.class, customResource.getConfigMapName(configMapId));
43-
assertThat(configMap).isNotNull();
44-
assertThat(configMap.getMetadata().getName())
45-
.isEqualTo(customResource.getConfigMapName(configMapId));
46-
assertThat(configMap.getData().get(MultipleDependentResourceConfigMap.DATA_KEY))
47-
.isEqualTo(String.valueOf(configMapId));
48-
});
49-
}
35+
void handlesCRUDOperations() {
36+
var res = extension.create(testResource());
37+
38+
await().untilAsserted(() -> {
39+
var cm1 = extension.get(ConfigMap.class, getConfigMapName(FIRST_CONFIG_MAP_ID));
40+
var cm2 = extension.get(ConfigMap.class, getConfigMapName(SECOND_CONFIG_MAP_ID));
41+
42+
assertThat(cm1).isNotNull();
43+
assertThat(cm2).isNotNull();
44+
assertThat(cm1.getData()).containsEntry(DATA_KEY, INITIAL_VALUE);
45+
assertThat(cm2.getData()).containsEntry(DATA_KEY, INITIAL_VALUE);
46+
});
47+
48+
res.getSpec().setValue(CHANGED_VALUE);
49+
res = extension.replace(res);
50+
51+
await().untilAsserted(() -> {
52+
var cm1 = extension.get(ConfigMap.class, getConfigMapName(FIRST_CONFIG_MAP_ID));
53+
var cm2 = extension.get(ConfigMap.class, getConfigMapName(SECOND_CONFIG_MAP_ID));
5054

51-
public MultipleDependentResourceCustomResource createTestCustomResource() {
52-
MultipleDependentResourceCustomResource resource =
53-
new MultipleDependentResourceCustomResource();
54-
resource.setMetadata(
55-
new ObjectMetaBuilder()
56-
.withName(TEST_RESOURCE_NAME)
57-
.withNamespace(operator.getNamespace())
58-
.build());
59-
return resource;
55+
assertThat(cm1.getData()).containsEntry(DATA_KEY, CHANGED_VALUE);
56+
assertThat(cm2.getData()).containsEntry(DATA_KEY, CHANGED_VALUE);
57+
});
58+
59+
extension.delete(res);
60+
61+
await().timeout(Duration.ofSeconds(120)).untilAsserted(() -> {
62+
var cm1 = extension.get(ConfigMap.class, getConfigMapName(FIRST_CONFIG_MAP_ID));
63+
var cm2 = extension.get(ConfigMap.class, getConfigMapName(SECOND_CONFIG_MAP_ID));
64+
65+
assertThat(cm1).isNull();
66+
assertThat(cm2).isNull();
67+
});
6068
}
6169

70+
MultipleDependentResourceCustomResource testResource() {
71+
var res = new MultipleDependentResourceCustomResource();
72+
res.setMetadata(new ObjectMetaBuilder()
73+
.withName("test1")
74+
.build());
75+
res.setSpec(new MultipleDependentResourceSpec());
76+
res.getSpec().setValue(INITIAL_VALUE);
77+
78+
return res;
79+
}
6280
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import java.time.Duration;
4+
import java.util.stream.IntStream;
5+
6+
import org.junit.jupiter.api.Test;
7+
import org.junit.jupiter.api.extension.RegisterExtension;
8+
9+
import io.fabric8.kubernetes.api.model.ConfigMap;
10+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
11+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
12+
import io.javaoperatorsdk.operator.sample.multipledependentresourcewithdiscriminator.MultipleDependentResourceConfigMap;
13+
import io.javaoperatorsdk.operator.sample.multipledependentresourcewithdiscriminator.MultipleDependentResourceCustomResourceWithDiscriminator;
14+
import io.javaoperatorsdk.operator.sample.multipledependentresourcewithdiscriminator.MultipleDependentResourceWithDiscriminatorReconciler;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.awaitility.Awaitility.await;
18+
19+
class MultipleDependentResourceWithNoDiscriminatorIT {
20+
21+
public static final String TEST_RESOURCE_NAME = "multipledependentresource-testresource";
22+
@RegisterExtension
23+
LocallyRunOperatorExtension operator =
24+
LocallyRunOperatorExtension.builder()
25+
.withReconciler(MultipleDependentResourceWithDiscriminatorReconciler.class)
26+
.waitForNamespaceDeletion(true)
27+
.build();
28+
29+
@Test
30+
void twoConfigMapsHaveBeenCreated() {
31+
MultipleDependentResourceCustomResourceWithDiscriminator customResource =
32+
createTestCustomResource();
33+
operator.create(customResource);
34+
35+
var reconciler =
36+
operator.getReconcilerOfType(MultipleDependentResourceWithDiscriminatorReconciler.class);
37+
38+
await().pollDelay(Duration.ofMillis(300))
39+
.until(() -> reconciler.getNumberOfExecutions() <= 1);
40+
41+
IntStream.of(MultipleDependentResourceWithDiscriminatorReconciler.FIRST_CONFIG_MAP_ID,
42+
MultipleDependentResourceWithDiscriminatorReconciler.SECOND_CONFIG_MAP_ID)
43+
.forEach(configMapId -> {
44+
ConfigMap configMap =
45+
operator.get(ConfigMap.class, customResource.getConfigMapName(configMapId));
46+
assertThat(configMap).isNotNull();
47+
assertThat(configMap.getMetadata().getName())
48+
.isEqualTo(customResource.getConfigMapName(configMapId));
49+
assertThat(configMap.getData().get(MultipleDependentResourceConfigMap.DATA_KEY))
50+
.isEqualTo(String.valueOf(configMapId));
51+
});
52+
}
53+
54+
public MultipleDependentResourceCustomResourceWithDiscriminator createTestCustomResource() {
55+
MultipleDependentResourceCustomResourceWithDiscriminator resource =
56+
new MultipleDependentResourceCustomResourceWithDiscriminator();
57+
resource.setMetadata(
58+
new ObjectMetaBuilder()
59+
.withName(TEST_RESOURCE_NAME)
60+
.withNamespace(operator.getNamespace())
61+
.build());
62+
return resource;
63+
}
64+
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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.ConfigMap;
9+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
10+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
11+
import io.javaoperatorsdk.operator.sample.multipledrsametypenodiscriminator.*;
12+
13+
import static io.javaoperatorsdk.operator.sample.multipledrsametypenodiscriminator.MultipleManagedDependentSameTypeNoDiscriminatorReconciler.DATA_KEY;
14+
import static org.assertj.core.api.Assertions.assertThat;
15+
import static org.awaitility.Awaitility.await;
16+
17+
public class MultipleManagedDependentNoDiscriminatorIT {
18+
19+
public static final String RESOURCE_NAME = "test1";
20+
public static final String INITIAL_VALUE = "initial_value";
21+
public static final String CHANGED_VALUE = "changed_value";
22+
23+
@RegisterExtension
24+
LocallyRunOperatorExtension extension =
25+
LocallyRunOperatorExtension.builder()
26+
.withReconciler(new MultipleManagedDependentSameTypeNoDiscriminatorReconciler())
27+
.build();
28+
29+
@Test
30+
void handlesCRUDOperations() {
31+
var res = extension.create(testResource());
32+
33+
await().untilAsserted(() -> {
34+
var cm1 = extension.get(ConfigMap.class,
35+
RESOURCE_NAME + MultipleManagedDependentNoDiscriminatorConfigMap1.NAME_SUFFIX);
36+
var cm2 = extension.get(ConfigMap.class,
37+
RESOURCE_NAME + MultipleManagedDependentNoDiscriminatorConfigMap2.NAME_SUFFIX);
38+
39+
assertThat(cm1).isNotNull();
40+
assertThat(cm2).isNotNull();
41+
assertThat(cm1.getData()).containsEntry(DATA_KEY, INITIAL_VALUE);
42+
assertThat(cm2.getData()).containsEntry(DATA_KEY, INITIAL_VALUE);
43+
});
44+
45+
res.getSpec().setValue(CHANGED_VALUE);
46+
res = extension.replace(res);
47+
48+
await().untilAsserted(() -> {
49+
var cm1 = extension.get(ConfigMap.class,
50+
RESOURCE_NAME + MultipleManagedDependentNoDiscriminatorConfigMap1.NAME_SUFFIX);
51+
var cm2 = extension.get(ConfigMap.class,
52+
RESOURCE_NAME + MultipleManagedDependentNoDiscriminatorConfigMap2.NAME_SUFFIX);
53+
54+
assertThat(cm1.getData()).containsEntry(DATA_KEY, CHANGED_VALUE);
55+
assertThat(cm2.getData()).containsEntry(DATA_KEY, CHANGED_VALUE);
56+
});
57+
58+
extension.delete(res);
59+
60+
await().timeout(Duration.ofSeconds(60)).untilAsserted(() -> {
61+
var cm1 = extension.get(ConfigMap.class,
62+
RESOURCE_NAME + MultipleManagedDependentNoDiscriminatorConfigMap1.NAME_SUFFIX);
63+
var cm2 = extension.get(ConfigMap.class,
64+
RESOURCE_NAME + MultipleManagedDependentNoDiscriminatorConfigMap2.NAME_SUFFIX);
65+
66+
assertThat(cm1).isNull();
67+
assertThat(cm2).isNull();
68+
});
69+
}
70+
71+
MultipleManagedDependentNoDiscriminatorCustomResource testResource() {
72+
var res = new MultipleManagedDependentNoDiscriminatorCustomResource();
73+
res.setMetadata(new ObjectMetaBuilder()
74+
.withName(RESOURCE_NAME)
75+
.build());
76+
res.setSpec(new MultipleManagedDependentNoDiscriminatorSpec());
77+
res.getSpec().setValue(INITIAL_VALUE);
78+
return res;
79+
}
80+
81+
}

0 commit comments

Comments
 (0)