Skip to content

Commit 2ad10e7

Browse files
committed
feat: distinguish resources based on desired state (#2252)
Signed-off-by: Attila Mészáros <[email protected]>
1 parent 45f6518 commit 2ad10e7

File tree

29 files changed

+672
-108
lines changed

29 files changed

+672
-108
lines changed

Diff for: docs/documentation/dependent-resources.md

+29-18
Original file line numberDiff line numberDiff line change
@@ -301,20 +301,31 @@ tests [here](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/op
301301

302302
When dealing with multiple dependent resources of same type, the dependent resource implementation
303303
needs to know which specific resource should be targeted when reconciling a given dependent
304-
resource, since there will be multiple instances of that type which could possibly be used, each
305-
associated with the same primary resource. In order to do this, JOSDK relies on the
306-
[resource discriminator](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceDiscriminator.java)
307-
concept. Resource discriminators uniquely identify the target resource of a dependent resource.
308-
In the managed Kubernetes dependent resources case, the discriminator can be declaratively set
309-
using the `@KubernetesDependent` annotation:
310-
311-
```java
312-
313-
@KubernetesDependent(resourceDiscriminator = ConfigMap1Discriminator.class)
314-
public class MultipleManagedDependentResourceConfigMap1 {
315-
//...
316-
}
317-
```
304+
resource, since there could be multiple instances of that type which could possibly be used, each
305+
associated with the same primary resource. In this situation, JOSDK automatically selects the appropriate secondary
306+
resource matching the desired state associated with the primary resource. This makes sense because the desired
307+
state computation already needs to be able to discriminate among multiple related secondary resources to tell JOSDK how
308+
they should be reconciled.
309+
310+
There might be casees, though, where it might be problematic to call the `desired` method several times (for example, because it is costly to do so), it is always possible to override this automated discrimination using several means:
311+
312+
- Implement your own `getSecondaryResource` method on your `DependentResource` implementation from scratch.
313+
- Override the `selectManagedSecondaryResource` method, if your `DependentResource` extends `AbstractDependentResource`.
314+
This should be relatively simple to override this method to optimize the matching to your needs. You can see an
315+
example of such an implementation in
316+
the [`ExternalWithStateDependentResource`](https://github.com/operator-framework/java-operator-sdk/blob/6cd0f884a7c9b60c81bd2d52da54adbd64d6e118/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/ExternalWithStateDependentResource.java#L43-L49)
317+
class.
318+
- Override the `managedSecondaryResourceID` method, if your `DependentResource` extends `KubernetesDependentResource`,
319+
where it's very often possible to easily determine the `ResourceID` of the secondary resource. This would probably be
320+
the easiest solution if you're working with Kubernetes resources.
321+
- Configure
322+
a [`ResourceDiscriminator`](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceDiscriminator.java)
323+
implementation for your `DependentResource`. This was the approach that was used before JOSDK v5 but should not be
324+
needed anymore as it is simpler and more efficient to override one the methods above instead of creating a separate
325+
class. Discriminators can be declaratively set when using managed Kubernetes dependent resources via
326+
the `resourceDiscriminator` field of the `@KubernetesDependent` annotation.
327+
328+
### Sharing an Event Source Between Dependent Resources
318329

319330
Dependent resources usually also provide event sources. When dealing with multiple dependents of
320331
the same type, one needs to decide whether these dependent resources should track the same
@@ -330,10 +341,10 @@ would look as follows:
330341
useEventSourceWithName = "configMapSource")
331342
```
332343

333-
A sample is provided as an integration test both
334-
for [managed](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleManagedDependentSameTypeIT.java)
335-
and
336-
for [standalone](https://github.com/java-operator-sdk/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleDependentResourceIT.java)
344+
A sample is provided as an integration test both:
345+
for [managed](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleManagedDependentNoDiscriminatorIT.java)
346+
347+
For [standalone](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleDependentResourceIT.java)
337348
cases.
338349

339350
## Bulk Dependent Resources

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;
@@ -97,8 +98,39 @@ protected ReconcileResult<R> reconcile(P primary, R actualResource, Context<P> c
9798

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

104136
private void throwIfNull(R desired, P primary, String descriptor) {
@@ -166,8 +198,7 @@ protected void handleDelete(P primary, R secondary, Context<P> context) {
166198
"handleDelete method must be implemented if Deleter trait is supported");
167199
}
168200

169-
public void setResourceDiscriminator(
170-
ResourceDiscriminator<R, P> resourceDiscriminator) {
201+
public void setResourceDiscriminator(ResourceDiscriminator<R, P> resourceDiscriminator) {
171202
this.resourceDiscriminator = resourceDiscriminator;
172203
}
173204

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

@@ -294,6 +295,29 @@ protected void addSecondaryToPrimaryMapperAnnotations(R desired, P primary, Stri
294295
}
295296
}
296297

298+
@Override
299+
protected Optional<R> selectManagedSecondaryResource(Set<R> secondaryResources, P primary,
300+
Context<P> context) {
301+
ResourceID managedResourceID = managedSecondaryResourceID(primary, context);
302+
return secondaryResources.stream()
303+
.filter(r -> r.getMetadata().getName().equals(managedResourceID.getName()) &&
304+
Objects.equals(r.getMetadata().getNamespace(),
305+
managedResourceID.getNamespace().orElse(null)))
306+
.findFirst();
307+
}
308+
309+
/**
310+
* Override this method in order to optimize and not compute the desired when selecting the target
311+
* secondary resource. Simply, a static ResourceID can be returned.
312+
*
313+
* @param primary resource
314+
* @param context of current reconciliation
315+
* @return id of the target managed resource
316+
*/
317+
protected ResourceID managedSecondaryResourceID(P primary, Context<P> context) {
318+
return ResourceID.fromResource(desired(primary, context));
319+
}
320+
297321
protected boolean addOwnerReference() {
298322
return garbageCollected;
299323
}

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+
}

0 commit comments

Comments
 (0)