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 376d745

Browse files
committedOct 25, 2022
feat: bulk dependent resources (#1448)
1 parent e331508 commit 376d745

29 files changed

+956
-67
lines changed
 

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/ResourceDiscriminator.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,4 @@
77
public interface ResourceDiscriminator<R, P extends HasMetadata> {
88

99
Optional<R> distinguish(Class<R> resource, P primary, Context<P> context);
10-
1110
}

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/ReconcileResult.java

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package io.javaoperatorsdk.operator.api.reconciler.dependent;
22

3-
import java.util.Optional;
3+
import java.util.*;
4+
import java.util.stream.Collectors;
45

56
import io.fabric8.kubernetes.api.model.HasMetadata;
67
import io.javaoperatorsdk.operator.processing.event.ResourceID;
78

89
public class ReconcileResult<R> {
910

10-
private final R resource;
11-
private final Operation operation;
11+
private final Map<R, Operation> resourceOperations;
1212

1313
public static <T> ReconcileResult<T> resourceCreated(T resource) {
1414
return new ReconcileResult<>(resource, Operation.CREATED);
@@ -22,25 +22,49 @@ public static <T> ReconcileResult<T> noOperation(T resource) {
2222
return new ReconcileResult<>(resource, Operation.NONE);
2323
}
2424

25+
@SafeVarargs
26+
public static <T> ReconcileResult<T> aggregatedResult(ReconcileResult<T>... results) {
27+
if (results == null) {
28+
throw new IllegalArgumentException("Should provide results to aggregate");
29+
}
30+
if (results.length == 1) {
31+
return results[0];
32+
}
33+
final Map<T, Operation> operations = new HashMap<>(results.length);
34+
for (ReconcileResult<T> res : results) {
35+
res.getSingleResource().ifPresent(r -> operations.put(r, res.getSingleOperation()));
36+
}
37+
return new ReconcileResult<>(operations);
38+
}
39+
2540
@Override
2641
public String toString() {
27-
return getResource()
28-
.map(r -> r instanceof HasMetadata ? ResourceID.fromResource((HasMetadata) r) : r)
29-
.orElse("no resource")
30-
+ " -> " + operation;
42+
return resourceOperations.entrySet().stream().collect(Collectors.toMap(
43+
e -> e instanceof HasMetadata ? ResourceID.fromResource((HasMetadata) e) : e,
44+
Map.Entry::getValue))
45+
.toString();
3146
}
3247

3348
private ReconcileResult(R resource, Operation operation) {
34-
this.resource = resource;
35-
this.operation = operation;
49+
resourceOperations = resource != null ? Map.of(resource, operation) : Collections.emptyMap();
50+
}
51+
52+
private ReconcileResult(Map<R, Operation> operations) {
53+
resourceOperations = Collections.unmodifiableMap(operations);
54+
}
55+
56+
public Optional<R> getSingleResource() {
57+
return resourceOperations.entrySet().stream().findFirst().map(Map.Entry::getKey);
3658
}
3759

38-
public Optional<R> getResource() {
39-
return Optional.ofNullable(resource);
60+
public Operation getSingleOperation() {
61+
return resourceOperations.entrySet().stream().findFirst().map(Map.Entry::getValue)
62+
.orElseThrow();
4063
}
4164

42-
public Operation getOperation() {
43-
return operation;
65+
@SuppressWarnings("unused")
66+
public Map<R, Operation> getResourceOperations() {
67+
return resourceOperations;
4468
}
4569

4670
public enum Operation {

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

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.javaoperatorsdk.operator.processing.dependent;
22

3+
import java.util.ArrayList;
4+
import java.util.List;
35
import java.util.Optional;
46

57
import org.slf4j.Logger;
@@ -20,25 +22,73 @@ public abstract class AbstractDependentResource<R, P extends HasMetadata>
2022

2123
protected final boolean creatable = this instanceof Creator;
2224
protected final boolean updatable = this instanceof Updater;
25+
protected final boolean bulk = this instanceof BulkDependentResource;
2326

2427
protected Creator<R, P> creator;
2528
protected Updater<R, P> updater;
29+
protected BulkDependentResource<R, P> bulkDependentResource;
2630

27-
private ResourceDiscriminator<R, P> resourceDiscriminator;
31+
private final List<ResourceDiscriminator<R, P>> resourceDiscriminator = new ArrayList<>(1);
2832

2933
@SuppressWarnings("unchecked")
3034
public AbstractDependentResource() {
3135
creator = creatable ? (Creator<R, P>) this : null;
3236
updater = updatable ? (Updater<R, P>) this : null;
37+
38+
bulkDependentResource = bulk ? (BulkDependentResource<R, P>) this : null;
3339
}
3440

3541
@Override
3642
public ReconcileResult<R> reconcile(P primary, Context<P> context) {
37-
Optional<R> maybeActual = getSecondaryResource(primary, context);
43+
if (bulk) {
44+
final var count = bulkDependentResource.count(primary, context);
45+
deleteBulkResourcesIfRequired(count, lastKnownBulkSize(), primary, context);
46+
adjustDiscriminators(count);
47+
@SuppressWarnings("unchecked")
48+
final ReconcileResult<R>[] results = new ReconcileResult[count];
49+
for (int i = 0; i < count; i++) {
50+
results[i] = reconcileIndexAware(primary, i, context);
51+
}
52+
return ReconcileResult.aggregatedResult(results);
53+
} else {
54+
return reconcileIndexAware(primary, 0, context);
55+
}
56+
}
57+
58+
protected void deleteBulkResourcesIfRequired(int targetCount, int actualCount, P primary,
59+
Context<P> context) {
60+
if (targetCount >= actualCount) {
61+
return;
62+
}
63+
for (int i = targetCount; i < actualCount; i++) {
64+
var resource = getSecondaryResourceIndexAware(primary, i, context);
65+
var index = i;
66+
resource.ifPresent(
67+
r -> bulkDependentResource.deleteBulkResourceWithIndex(primary, r, index, context));
68+
}
69+
}
70+
71+
private void adjustDiscriminators(int count) {
72+
if (resourceDiscriminator.size() == count) {
73+
return;
74+
}
75+
if (resourceDiscriminator.size() < count) {
76+
for (int i = resourceDiscriminator.size(); i < count; i++) {
77+
resourceDiscriminator.add(bulkDependentResource.getResourceDiscriminator(i));
78+
}
79+
}
80+
if (resourceDiscriminator.size() > count) {
81+
resourceDiscriminator.subList(count, resourceDiscriminator.size()).clear();
82+
}
83+
}
84+
85+
protected ReconcileResult<R> reconcileIndexAware(P primary, int i, Context<P> context) {
86+
Optional<R> maybeActual = bulk ? getSecondaryResourceIndexAware(primary, i, context)
87+
: getSecondaryResource(primary, context);
3888
if (creatable || updatable) {
3989
if (maybeActual.isEmpty()) {
4090
if (creatable) {
41-
var desired = desired(primary, context);
91+
var desired = desiredIndexAware(primary, i, context);
4292
throwIfNull(desired, primary, "Desired");
4393
logForOperation("Creating", primary, desired);
4494
var createdResource = handleCreate(desired, primary, context);
@@ -47,9 +97,15 @@ public ReconcileResult<R> reconcile(P primary, Context<P> context) {
4797
} else {
4898
final var actual = maybeActual.get();
4999
if (updatable) {
50-
final var match = updater.match(actual, primary, context);
100+
final Matcher.Result<R> match;
101+
if (bulk) {
102+
match = updater.match(actual, primary, i, context);
103+
} else {
104+
match = updater.match(actual, primary, context);
105+
}
51106
if (!match.matched()) {
52-
final var desired = match.computedDesired().orElseGet(() -> desired(primary, context));
107+
final var desired =
108+
match.computedDesired().orElse(desiredIndexAware(primary, i, context));
53109
throwIfNull(desired, primary, "Desired");
54110
logForOperation("Updating", primary, desired);
55111
var updatedResource = handleUpdate(actual, desired, primary, context);
@@ -67,9 +123,18 @@ public ReconcileResult<R> reconcile(P primary, Context<P> context) {
67123
return ReconcileResult.noOperation(maybeActual.orElse(null));
68124
}
69125

126+
private R desiredIndexAware(P primary, int i, Context<P> context) {
127+
return bulk ? desired(primary, i, context)
128+
: desired(primary, context);
129+
}
130+
70131
public Optional<R> getSecondaryResource(P primary, Context<P> context) {
71-
return resourceDiscriminator == null ? context.getSecondaryResource(resourceType())
72-
: resourceDiscriminator.distinguish(resourceType(), primary, context);
132+
return resourceDiscriminator.isEmpty() ? context.getSecondaryResource(resourceType())
133+
: resourceDiscriminator.get(0).distinguish(resourceType(), primary, context);
134+
}
135+
136+
protected Optional<R> getSecondaryResourceIndexAware(P primary, int index, Context<P> context) {
137+
return context.getSecondaryResource(resourceType(), resourceDiscriminator.get(index));
73138
}
74139

75140
private void throwIfNull(R desired, P primary, String descriptor) {
@@ -97,7 +162,7 @@ protected R handleCreate(R desired, P primary, Context<P> context) {
97162
}
98163

99164
/**
100-
* Allows sub-classes to perform additional processing (e.g. caching) on the created resource if
165+
* Allows subclasses to perform additional processing (e.g. caching) on the created resource if
101166
* needed.
102167
*
103168
* @param primaryResourceId the {@link ResourceID} of the primary resource associated with the
@@ -129,12 +194,29 @@ protected R desired(P primary, Context<P> context) {
129194
"desired method must be implemented if this DependentResource can be created and/or updated");
130195
}
131196

132-
public void setResourceDiscriminator(
197+
protected R desired(P primary, int index, Context<P> context) {
198+
throw new IllegalStateException(
199+
"Must be implemented for bulk DependentResource creation");
200+
}
201+
202+
public AbstractDependentResource<R, P> setResourceDiscriminator(
133203
ResourceDiscriminator<R, P> resourceDiscriminator) {
134-
this.resourceDiscriminator = resourceDiscriminator;
204+
if (resourceDiscriminator != null) {
205+
this.resourceDiscriminator.add(resourceDiscriminator);
206+
}
207+
return this;
135208
}
136209

137210
public ResourceDiscriminator<R, P> getResourceDiscriminator() {
138-
return resourceDiscriminator;
211+
if (this.resourceDiscriminator.isEmpty()) {
212+
return null;
213+
} else {
214+
return this.resourceDiscriminator.get(0);
215+
}
139216
}
217+
218+
protected int lastKnownBulkSize() {
219+
return resourceDiscriminator.size();
220+
}
221+
140222
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package io.javaoperatorsdk.operator.processing.dependent;
2+
3+
import io.fabric8.kubernetes.api.model.HasMetadata;
4+
import io.javaoperatorsdk.operator.api.reconciler.Context;
5+
import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
6+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
7+
8+
/**
9+
* Manages dynamic number of resources created for a primary resource. Since the point of a bulk
10+
* dependent resource is to manage the number of secondary resources dynamically it implement
11+
* {@link Creator} and {@link Deleter} interfaces out of the box. A concrete dependent resource can
12+
* implement additionally also {@link Updater}.
13+
*/
14+
public interface BulkDependentResource<R, P extends HasMetadata> extends Creator<R, P>, Deleter<P> {
15+
16+
/**
17+
* @return number of resources to create
18+
*/
19+
int count(P primary, Context<P> context);
20+
21+
R desired(P primary, int index, Context<P> context);
22+
23+
/**
24+
* Used to delete resource if the desired count is lower than the actual count of a resource.
25+
*
26+
* @param primary resource
27+
* @param resource actual resource from the cache for the index
28+
* @param i index of the resource
29+
* @param context actual context
30+
*/
31+
void deleteBulkResourceWithIndex(P primary, R resource, int i, Context<P> context);
32+
33+
ResourceDiscriminator<R, P> getResourceDiscriminator(int index);
34+
35+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.javaoperatorsdk.operator.processing.dependent;
2+
3+
import io.fabric8.kubernetes.api.model.HasMetadata;
4+
import io.javaoperatorsdk.operator.api.reconciler.Context;
5+
6+
/**
7+
* Helper for the Bulk Dependent Resources to make it more explicit that bulk needs to only
8+
* implement the index aware match method.
9+
*
10+
* @param <R> secondary resource type
11+
* @param <P> primary resource type
12+
*/
13+
public interface BulkUpdater<R, P extends HasMetadata> extends Updater<R, P> {
14+
15+
default Matcher.Result<R> match(R actualResource, P primary, Context<P> context) {
16+
throw new IllegalStateException();
17+
}
18+
19+
Matcher.Result<R> match(R actualResource, P primary, int index, Context<P> context);
20+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@ public Result<R> match(R actualResource, P primary, Context<P> context) {
1616
var desired = abstractDependentResource.desired(primary, context);
1717
return Result.computed(actualResource.equals(desired), desired);
1818
}
19+
20+
@Override
21+
public Result<R> match(R actualResource, P primary, int index, Context<P> context) {
22+
var desired = abstractDependentResource.desired(primary, index, context);
23+
return Result.computed(actualResource.equals(desired), desired);
24+
}
1925
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,19 @@ public Optional<T> computedDesired() {
9595
* {@link Result#computed(boolean, Object)})
9696
*/
9797
Result<R> match(R actualResource, P primary, Context<P> context);
98+
99+
/**
100+
* Determines whether the specified secondary resource matches the desired state with target index
101+
* of a bulk resource as defined from the specified primary resource, given the specified
102+
* {@link Context}.
103+
*
104+
* @param actualResource the resource we want to determine whether it's matching the desired state
105+
* @param primary the primary resource from which the desired state is inferred
106+
* @param context the context in which the resource is being matched
107+
* @return a {@link Result} encapsulating whether the resource matched its desired state and this
108+
* associated state if it was computed as part of the matching process. Use the static
109+
* convenience methods ({@link Result#nonComputed(boolean)} and
110+
* {@link Result#computed(boolean, Object)})
111+
*/
112+
Result<R> match(R actualResource, P primary, int index, Context<P> context);
98113
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ public interface Updater<R, P extends HasMetadata> {
88
R update(R actual, R desired, P primary, Context<P> context);
99

1010
Result<R> match(R actualResource, P primary, Context<P> context);
11+
12+
default Result<R> match(R actualResource, P primary, int index, Context<P> context) {
13+
throw new IllegalStateException("Implement this for bulk matching");
14+
}
1115
}

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

Lines changed: 70 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,42 @@ private GenericKubernetesResourceMatcher(KubernetesDependentResource<R, P> depen
2424
static <R extends HasMetadata, P extends HasMetadata> Matcher<R, P> matcherFor(
2525
Class<R> resourceType, KubernetesDependentResource<R, P> dependentResource) {
2626
if (Secret.class.isAssignableFrom(resourceType)) {
27-
return (actual, primary, context) -> {
28-
final var desired = dependentResource.desired(primary, context);
29-
return Result.computed(
30-
ResourceComparators.compareSecretData((Secret) desired, (Secret) actual), desired);
27+
return new Matcher<>() {
28+
@Override
29+
public Result<R> match(R actualResource, P primary, Context<P> context) {
30+
final var desired = dependentResource.desired(primary, context);
31+
return Result.computed(
32+
ResourceComparators.compareSecretData((Secret) desired, (Secret) actualResource),
33+
desired);
34+
}
35+
36+
@Override
37+
public Result<R> match(R actualResource, P primary, int index, Context<P> context) {
38+
final var desired = dependentResource.desired(primary, index, context);
39+
return Result.computed(
40+
ResourceComparators.compareSecretData((Secret) desired, (Secret) actualResource),
41+
desired);
42+
}
3143
};
3244
} else if (ConfigMap.class.isAssignableFrom(resourceType)) {
33-
return (actual, primary, context) -> {
34-
final var desired = dependentResource.desired(primary, context);
35-
return Result.computed(
36-
ResourceComparators.compareConfigMapData((ConfigMap) desired, (ConfigMap) actual),
37-
desired);
45+
return new Matcher<>() {
46+
@Override
47+
public Result<R> match(R actualResource, P primary, Context<P> context) {
48+
final var desired = dependentResource.desired(primary, context);
49+
return Result.computed(
50+
ResourceComparators.compareConfigMapData((ConfigMap) desired,
51+
(ConfigMap) actualResource),
52+
desired);
53+
}
54+
55+
@Override
56+
public Result<R> match(R actualResource, P primary, int index, Context<P> context) {
57+
final var desired = dependentResource.desired(primary, index, context);
58+
return Result.computed(
59+
ResourceComparators.compareConfigMapData((ConfigMap) desired,
60+
(ConfigMap) actualResource),
61+
desired);
62+
}
3863
};
3964
} else {
4065
return new GenericKubernetesResourceMatcher(dependentResource);
@@ -43,32 +68,18 @@ static <R extends HasMetadata, P extends HasMetadata> Matcher<R, P> matcherFor(
4368

4469
@Override
4570
public Result<R> match(R actualResource, P primary, Context<P> context) {
46-
return match(dependentResource, actualResource, primary, context, false);
71+
var desired = dependentResource.desired(primary, context);
72+
return match(desired, actualResource, false);
4773
}
4874

49-
/**
50-
* Determines whether the specified actual resource matches the desired state defined by the
51-
* specified {@link KubernetesDependentResource} based on the observed state of the associated
52-
* specified primary resource.
53-
*
54-
* @param dependentResource the {@link KubernetesDependentResource} implementation used to
55-
* computed the desired state associated with the specified primary resource
56-
* @param actualResource the observed dependent resource for which we want to determine whether it
57-
* matches the desired state or not
58-
* @param primary the primary resource from which we want to compute the desired state
59-
* @param context the {@link Context} instance within which this method is called
60-
* @param considerMetadata {@code true} to consider the metadata of the actual resource when
61-
* determining if it matches the desired state, {@code false} if matching should occur only
62-
* considering the spec of the resources
63-
* @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object
64-
* @param <R> the type of resource we want to determine whether they match or not
65-
* @param <P> the type of primary resources associated with the secondary resources we want to
66-
* match
67-
*/
68-
public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
69-
KubernetesDependentResource<R, P> dependentResource, R actualResource, P primary,
70-
Context<P> context, boolean considerMetadata) {
71-
final var desired = dependentResource.desired(primary, context);
75+
@Override
76+
public Result<R> match(R actualResource, P primary, int index, Context<P> context) {
77+
var desired = dependentResource.desired(primary, index, context);
78+
return match(desired, actualResource, false);
79+
}
80+
81+
public static <R extends HasMetadata> Result<R> match(
82+
R desired, R actualResource, boolean considerMetadata) {
7283
if (considerMetadata) {
7384
final var desiredMetadata = desired.getMetadata();
7485
final var actualMetadata = actualResource.getMetadata();
@@ -95,4 +106,30 @@ public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
95106
}
96107
return Result.computed(true, desired);
97108
}
109+
110+
/**
111+
* Determines whether the specified actual resource matches the desired state defined by the
112+
* specified {@link KubernetesDependentResource} based on the observed state of the associated
113+
* specified primary resource.
114+
*
115+
* @param dependentResource the {@link KubernetesDependentResource} implementation used to
116+
* computed the desired state associated with the specified primary resource
117+
* @param actualResource the observed dependent resource for which we want to determine whether it
118+
* matches the desired state or not
119+
* @param primary the primary resource from which we want to compute the desired state
120+
* @param context the {@link Context} instance within which this method is called
121+
* @param considerMetadata {@code true} to consider the metadata of the actual resource when
122+
* determining if it matches the desired state, {@code false} if matching should occur only
123+
* considering the spec of the resources
124+
* @return a {@link io.javaoperatorsdk.operator.processing.dependent.Matcher.Result} object
125+
* @param <R> the type of resource we want to determine whether they match or not
126+
* @param <P> the type of primary resources associated with the secondary resources we want to
127+
* match
128+
*/
129+
public static <R extends HasMetadata, P extends HasMetadata> Result<R> match(
130+
KubernetesDependentResource<R, P> dependentResource, R actualResource, P primary,
131+
Context<P> context, boolean considerMetadata) {
132+
final var desired = dependentResource.desired(primary, context);
133+
return match(desired, actualResource, considerMetadata);
134+
}
98135
}

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,21 @@ public Result<R> match(R actualResource, P primary, Context<P> context) {
134134
return matcher.match(actualResource, primary, context);
135135
}
136136

137+
public Result<R> match(R actualResource, P primary, int index, Context<P> context) {
138+
return matcher.match(actualResource, primary, index, context);
139+
}
140+
137141
public void delete(P primary, Context<P> context) {
138-
getSecondaryResource(primary, context).ifPresent(r -> client.resource(r).delete());
142+
if (bulk) {
143+
deleteBulkResourcesIfRequired(0, lastKnownBulkSize(), primary, context);
144+
} else {
145+
var resource = getSecondaryResource(primary, context);
146+
resource.ifPresent(r -> client.resource(r).delete());
147+
}
148+
}
149+
150+
public void deleteBulkResourceWithIndex(P primary, R resource, int i, Context<P> context) {
151+
client.resource(resource).delete();
139152
}
140153

141154
@SuppressWarnings("unchecked")
@@ -149,9 +162,7 @@ protected Resource<R> prepare(R desired, P primary, String actionName) {
149162
} else if (useDefaultAnnotationsToIdentifyPrimary()) {
150163
addDefaultSecondaryToPrimaryMapperAnnotations(desired, primary);
151164
}
152-
Class<R> targetClass = (Class<R>) desired.getClass();
153-
return client.resources(targetClass).inNamespace(desired.getMetadata().getNamespace())
154-
.resource(desired);
165+
return client.resource(desired).inNamespace(desired.getMetadata().getNamespace());
155166
}
156167

157168
@Override
@@ -163,8 +174,10 @@ protected InformerEventSource<R, P> createEventSource(EventSourceContext<P> cont
163174
onUpdateFilter = kubernetesDependentResourceConfig.onUpdateFilter();
164175
onDeleteFilter = kubernetesDependentResourceConfig.onDeleteFilter();
165176
genericFilter = kubernetesDependentResourceConfig.genericFilter();
166-
setResourceDiscriminator(kubernetesDependentResourceConfig.getResourceDiscriminator());
167-
177+
var discriminator = kubernetesDependentResourceConfig.getResourceDiscriminator();
178+
if (discriminator != null) {
179+
setResourceDiscriminator(discriminator);
180+
}
168181
configureWith(kubernetesDependentResourceConfig.labelSelector(),
169182
kubernetesDependentResourceConfig.namespaces(),
170183
!kubernetesDependentResourceConfig.wereNamespacesConfigured(), context);
@@ -215,6 +228,11 @@ protected R desired(P primary, Context<P> context) {
215228
return super.desired(primary, context);
216229
}
217230

231+
@Override
232+
protected R desired(P primary, int index, Context<P> context) {
233+
return super.desired(primary, index, context);
234+
}
235+
218236
private void prepareEventFiltering(R desired, ResourceID resourceID) {
219237
eventSource().prepareForCreateOrUpdateEventFiltering(resourceID, desired);
220238
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ public OnAddFilter onAddFilter() {
7575
return onAddFilter;
7676
}
7777

78-
7978
public OnUpdateFilter<R> onUpdateFilter() {
8079
return onUpdateFilter;
8180
}
@@ -92,4 +91,10 @@ public GenericFilter<R> genericFilter() {
9291
public ResourceDiscriminator getResourceDiscriminator() {
9392
return resourceDiscriminator;
9493
}
94+
95+
public <P> KubernetesDependentResourceConfig<R> setResourceDiscriminator(
96+
ResourceDiscriminator<R, ?> resourceDiscriminator) {
97+
this.resourceDiscriminator = resourceDiscriminator;
98+
return this;
99+
}
95100
}

‎operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,6 @@ public void run() {
125125
}
126126
}
127127

128-
129128
private synchronized void handleDependentCleaned(
130129
DependentResourceNode<?, P> dependentResourceNode) {
131130
var dependOns = dependentResourceNode.getDependsOn();
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.javaoperatorsdk.operator.bulkdependent;
2+
3+
import org.junit.jupiter.api.extension.RegisterExtension;
4+
5+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
6+
import io.javaoperatorsdk.operator.sample.bulkdependent.ManagedDeleterBulkReconciler;
7+
8+
public class BulkDependentDeleterIT extends BulkDependentTestBase {
9+
10+
@RegisterExtension
11+
LocallyRunOperatorExtension extension =
12+
LocallyRunOperatorExtension.builder().withReconciler(new ManagedDeleterBulkReconciler())
13+
.build();
14+
15+
@Override
16+
LocallyRunOperatorExtension extension() {
17+
return extension;
18+
}
19+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package io.javaoperatorsdk.operator.bulkdependent;
2+
3+
import java.time.Duration;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import io.fabric8.kubernetes.api.model.ObjectMeta;
8+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
9+
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;
10+
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestSpec;
11+
import io.javaoperatorsdk.operator.sample.bulkdependent.ConfigMapDeleterBulkDependentResource;
12+
13+
import static io.javaoperatorsdk.operator.sample.bulkdependent.ConfigMapDeleterBulkDependentResource.LABEL_KEY;
14+
import static io.javaoperatorsdk.operator.sample.bulkdependent.ConfigMapDeleterBulkDependentResource.LABEL_VALUE;
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.awaitility.Awaitility.await;
17+
18+
public abstract class BulkDependentTestBase {
19+
20+
public static final String TEST_RESOURCE_NAME = "test";
21+
public static final int INITIAL_NUMBER_OF_CONFIG_MAPS = 3;
22+
public static final String INITIAL_ADDITIONAL_DATA = "initialData";
23+
public static final String NEW_VERSION_OF_ADDITIONAL_DATA = "newVersionOfAdditionalData";
24+
25+
@Test
26+
public void managesBulkConfigMaps() {
27+
extension().create(testResource());
28+
assertNumberOfConfigMaps(3);
29+
30+
updateSpecWithNumber(1);
31+
assertNumberOfConfigMaps(1);
32+
33+
updateSpecWithNumber(5);
34+
assertNumberOfConfigMaps(5);
35+
36+
extension().delete(testResource());
37+
assertNumberOfConfigMaps(0);
38+
}
39+
40+
@Test
41+
public void updatesData() {
42+
extension().create(testResource());
43+
assertNumberOfConfigMaps(3);
44+
assertAdditionalDataOnConfigMaps(INITIAL_ADDITIONAL_DATA);
45+
46+
updateSpecWithNewAdditionalData(NEW_VERSION_OF_ADDITIONAL_DATA);
47+
assertAdditionalDataOnConfigMaps(NEW_VERSION_OF_ADDITIONAL_DATA);
48+
}
49+
50+
private void assertNumberOfConfigMaps(int n) {
51+
// this test was failing with a lower timeout on GitHub, probably the garbage collection was
52+
// slower there.
53+
await().atMost(Duration.ofSeconds(30))
54+
.untilAsserted(() -> {
55+
var cms =
56+
extension().getKubernetesClient().configMaps().inNamespace(extension().getNamespace())
57+
.withLabel(LABEL_KEY, LABEL_VALUE)
58+
.list().getItems();
59+
assertThat(cms).withFailMessage("Number of items is still: " + cms.size())
60+
.hasSize(n);
61+
});
62+
}
63+
64+
private void assertAdditionalDataOnConfigMaps(String expectedValue) {
65+
await().atMost(Duration.ofSeconds(30))
66+
.untilAsserted(() -> {
67+
var cms =
68+
extension().getKubernetesClient().configMaps().inNamespace(extension().getNamespace())
69+
.withLabel(LABEL_KEY, LABEL_VALUE)
70+
.list().getItems();
71+
cms.forEach(cm -> {
72+
assertThat(cm.getData().get(ConfigMapDeleterBulkDependentResource.ADDITIONAL_DATA_KEY))
73+
.isEqualTo(expectedValue);
74+
});
75+
});
76+
}
77+
78+
public static BulkDependentTestCustomResource testResource() {
79+
BulkDependentTestCustomResource cr = new BulkDependentTestCustomResource();
80+
cr.setMetadata(new ObjectMeta());
81+
cr.getMetadata().setName(TEST_RESOURCE_NAME);
82+
cr.setSpec(new BulkDependentTestSpec());
83+
cr.getSpec().setNumberOfResources(INITIAL_NUMBER_OF_CONFIG_MAPS);
84+
cr.getSpec().setAdditionalData(INITIAL_ADDITIONAL_DATA);
85+
return cr;
86+
}
87+
88+
private void updateSpecWithNewAdditionalData(String data) {
89+
var resource = testResource();
90+
resource.getSpec().setAdditionalData(data);
91+
extension().replace(resource);
92+
}
93+
94+
public static void updateSpecWithNewAdditionalData(LocallyRunOperatorExtension extension,
95+
String data) {
96+
var resource = testResource();
97+
resource.getSpec().setAdditionalData(data);
98+
extension.replace(resource);
99+
}
100+
101+
private void updateSpecWithNumber(int n) {
102+
var resource = testResource();
103+
resource.getSpec().setNumberOfResources(n);
104+
extension().replace(resource);
105+
}
106+
107+
public static void updateSpecWithNumber(LocallyRunOperatorExtension extension, int n) {
108+
var resource = testResource();
109+
resource.getSpec().setNumberOfResources(n);
110+
extension.replace(resource);
111+
}
112+
113+
abstract LocallyRunOperatorExtension extension();
114+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.javaoperatorsdk.operator.bulkdependent;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.extension.RegisterExtension;
5+
6+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
7+
import io.javaoperatorsdk.operator.sample.bulkdependent.external.ExternalBulkResourceReconciler;
8+
import io.javaoperatorsdk.operator.sample.bulkdependent.external.ExternalServiceMock;
9+
10+
import static io.javaoperatorsdk.operator.bulkdependent.BulkDependentTestBase.*;
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
import static org.awaitility.Awaitility.await;
13+
14+
class BulkExternalDependentIT {
15+
16+
@RegisterExtension
17+
LocallyRunOperatorExtension extension =
18+
LocallyRunOperatorExtension.builder().withReconciler(new ExternalBulkResourceReconciler())
19+
.build();
20+
21+
ExternalServiceMock externalServiceMock = ExternalServiceMock.getInstance();
22+
23+
@Test
24+
void managesExternalBulkResources() {
25+
extension.create(testResource());
26+
assertResourceNumberAndData(3, INITIAL_ADDITIONAL_DATA);
27+
28+
updateSpecWithNumber(extension, 1);
29+
assertResourceNumberAndData(1, INITIAL_ADDITIONAL_DATA);
30+
31+
updateSpecWithNumber(extension, 5);
32+
assertResourceNumberAndData(5, INITIAL_ADDITIONAL_DATA);
33+
34+
extension.delete(testResource());
35+
assertResourceNumberAndData(0, INITIAL_ADDITIONAL_DATA);
36+
}
37+
38+
39+
@Test
40+
void handlesResourceUpdates() {
41+
extension.create(testResource());
42+
assertResourceNumberAndData(3, INITIAL_ADDITIONAL_DATA);
43+
44+
updateSpecWithNewAdditionalData(extension, NEW_VERSION_OF_ADDITIONAL_DATA);
45+
assertResourceNumberAndData(3, NEW_VERSION_OF_ADDITIONAL_DATA);
46+
}
47+
48+
private void assertResourceNumberAndData(int n, String data) {
49+
await().untilAsserted(() -> {
50+
var resources = externalServiceMock.listResources();
51+
assertThat(resources).hasSize(n);
52+
assertThat(resources).allMatch(r -> r.getData().equals(data));
53+
});
54+
}
55+
56+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.javaoperatorsdk.operator.bulkdependent;
2+
3+
import org.junit.jupiter.api.extension.RegisterExtension;
4+
5+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
6+
import io.javaoperatorsdk.operator.sample.bulkdependent.ManagedBulkDependentReconciler;
7+
8+
class ManagedBulkDependentIT extends BulkDependentTestBase {
9+
10+
@RegisterExtension
11+
LocallyRunOperatorExtension extension =
12+
LocallyRunOperatorExtension.builder().withReconciler(new ManagedBulkDependentReconciler())
13+
.build();
14+
15+
16+
@Override
17+
LocallyRunOperatorExtension extension() {
18+
return extension;
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.javaoperatorsdk.operator.bulkdependent;
2+
3+
import org.junit.jupiter.api.extension.RegisterExtension;
4+
5+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
6+
import io.javaoperatorsdk.operator.sample.bulkdependent.StandaloneBulkDependentReconciler;
7+
8+
class StandaloneBulkDependentIT extends BulkDependentTestBase {
9+
10+
@RegisterExtension
11+
LocallyRunOperatorExtension extension =
12+
LocallyRunOperatorExtension.builder().withReconciler(new StandaloneBulkDependentReconciler())
13+
.build();
14+
15+
@Override
16+
LocallyRunOperatorExtension extension() {
17+
return extension;
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent;
2+
3+
import io.fabric8.kubernetes.api.model.Namespaced;
4+
import io.fabric8.kubernetes.client.CustomResource;
5+
import io.fabric8.kubernetes.model.annotation.Group;
6+
import io.fabric8.kubernetes.model.annotation.ShortNames;
7+
import io.fabric8.kubernetes.model.annotation.Version;
8+
9+
@Group("sample.javaoperatorsdk")
10+
@Version("v1")
11+
@ShortNames("sbd")
12+
public class BulkDependentTestCustomResource
13+
extends CustomResource<BulkDependentTestSpec, Void>
14+
implements Namespaced {
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent;
2+
3+
public class BulkDependentTestSpec {
4+
5+
private Integer numberOfResources;
6+
private String additionalData;
7+
8+
public Integer getNumberOfResources() {
9+
return numberOfResources;
10+
}
11+
12+
public BulkDependentTestSpec setNumberOfResources(Integer numberOfResources) {
13+
this.numberOfResources = numberOfResources;
14+
return this;
15+
}
16+
17+
public BulkDependentTestSpec setAdditionalData(String additionalData) {
18+
this.additionalData = additionalData;
19+
return this;
20+
}
21+
22+
public String getAdditionalData() {
23+
return additionalData;
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent;
2+
3+
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
4+
5+
public class CRUDConfigMapBulkDependentResource extends ConfigMapDeleterBulkDependentResource
6+
implements GarbageCollected<BulkDependentTestCustomResource> {
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent;
2+
3+
import java.util.Map;
4+
import java.util.Optional;
5+
import java.util.stream.Collectors;
6+
7+
import io.fabric8.kubernetes.api.model.ConfigMap;
8+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
9+
import io.javaoperatorsdk.operator.api.reconciler.Context;
10+
import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
11+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
12+
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
13+
import io.javaoperatorsdk.operator.processing.dependent.Creator;
14+
import io.javaoperatorsdk.operator.processing.dependent.Updater;
15+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
16+
17+
/**
18+
* Not using CRUDKubernetesDependentResource so the delete functionality can be tested.
19+
*/
20+
public class ConfigMapDeleterBulkDependentResource
21+
extends
22+
KubernetesDependentResource<ConfigMap, BulkDependentTestCustomResource>
23+
implements Creator<ConfigMap, BulkDependentTestCustomResource>,
24+
Updater<ConfigMap, BulkDependentTestCustomResource>,
25+
Deleter<BulkDependentTestCustomResource>,
26+
BulkDependentResource<ConfigMap, BulkDependentTestCustomResource> {
27+
28+
public static final String LABEL_KEY = "bulk";
29+
public static final String LABEL_VALUE = "true";
30+
public static final String ADDITIONAL_DATA_KEY = "additionalData";
31+
32+
public ConfigMapDeleterBulkDependentResource() {
33+
super(ConfigMap.class);
34+
}
35+
36+
@Override
37+
public ConfigMap desired(BulkDependentTestCustomResource primary,
38+
int index, Context<BulkDependentTestCustomResource> context) {
39+
ConfigMap configMap = new ConfigMap();
40+
configMap.setMetadata(new ObjectMetaBuilder()
41+
.withName(primary.getMetadata().getName() + "-" + index)
42+
.withNamespace(primary.getMetadata().getNamespace())
43+
.withLabels(Map.of(LABEL_KEY, LABEL_VALUE))
44+
.build());
45+
configMap.setData(
46+
Map.of("number", "" + index, ADDITIONAL_DATA_KEY, primary.getSpec().getAdditionalData()));
47+
return configMap;
48+
}
49+
50+
@Override
51+
public int count(BulkDependentTestCustomResource primary,
52+
Context<BulkDependentTestCustomResource> context) {
53+
return primary.getSpec().getNumberOfResources();
54+
}
55+
56+
@Override
57+
public ResourceDiscriminator<ConfigMap, BulkDependentTestCustomResource> getResourceDiscriminator(
58+
int index) {
59+
return (resource, primary, context) -> {
60+
var resources = context.getSecondaryResources(resource).stream()
61+
.filter(r -> r.getMetadata().getName().endsWith("-" + index))
62+
.collect(Collectors.toList());
63+
if (resources.isEmpty()) {
64+
return Optional.empty();
65+
} else if (resources.size() > 1) {
66+
throw new IllegalStateException("More than one resource found for index:" + index);
67+
} else {
68+
return Optional.of(resources.get(0));
69+
}
70+
};
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent;
2+
3+
import java.util.concurrent.atomic.AtomicInteger;
4+
5+
import io.javaoperatorsdk.operator.api.reconciler.Context;
6+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
7+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
8+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
9+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
10+
11+
@ControllerConfiguration(dependents = @Dependent(type = CRUDConfigMapBulkDependentResource.class))
12+
public class ManagedBulkDependentReconciler
13+
implements Reconciler<BulkDependentTestCustomResource> {
14+
15+
private final AtomicInteger numberOfExecutions = new AtomicInteger(0);
16+
17+
@Override
18+
public UpdateControl<BulkDependentTestCustomResource> reconcile(
19+
BulkDependentTestCustomResource resource,
20+
Context<BulkDependentTestCustomResource> context) throws Exception {
21+
22+
numberOfExecutions.addAndGet(1);
23+
return UpdateControl.noUpdate();
24+
}
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent;
2+
3+
import io.javaoperatorsdk.operator.api.reconciler.Context;
4+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
5+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
6+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
7+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
8+
9+
@ControllerConfiguration(
10+
dependents = @Dependent(type = ConfigMapDeleterBulkDependentResource.class))
11+
public class ManagedDeleterBulkReconciler implements Reconciler<BulkDependentTestCustomResource> {
12+
@Override
13+
public UpdateControl<BulkDependentTestCustomResource> reconcile(
14+
BulkDependentTestCustomResource resource,
15+
Context<BulkDependentTestCustomResource> context)
16+
throws Exception {
17+
18+
return UpdateControl.noUpdate();
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent;
2+
3+
import java.util.Map;
4+
import java.util.concurrent.atomic.AtomicInteger;
5+
6+
import io.fabric8.kubernetes.client.KubernetesClient;
7+
import io.javaoperatorsdk.operator.api.reconciler.*;
8+
import io.javaoperatorsdk.operator.junit.KubernetesClientAware;
9+
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
10+
import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider;
11+
12+
@ControllerConfiguration
13+
public class StandaloneBulkDependentReconciler
14+
implements Reconciler<BulkDependentTestCustomResource>, TestExecutionInfoProvider,
15+
EventSourceInitializer<BulkDependentTestCustomResource>, KubernetesClientAware {
16+
17+
private final AtomicInteger numberOfExecutions = new AtomicInteger(0);
18+
19+
private ConfigMapDeleterBulkDependentResource dependent;
20+
private KubernetesClient kubernetesClient;
21+
22+
public StandaloneBulkDependentReconciler() {
23+
dependent = new CRUDConfigMapBulkDependentResource();
24+
}
25+
26+
@Override
27+
public UpdateControl<BulkDependentTestCustomResource> reconcile(
28+
BulkDependentTestCustomResource resource,
29+
Context<BulkDependentTestCustomResource> context) {
30+
numberOfExecutions.addAndGet(1);
31+
32+
dependent.reconcile(resource, context);
33+
34+
return UpdateControl.noUpdate();
35+
}
36+
37+
public int getNumberOfExecutions() {
38+
return numberOfExecutions.get();
39+
}
40+
41+
@Override
42+
public Map<String, EventSource> prepareEventSources(
43+
EventSourceContext<BulkDependentTestCustomResource> context) {
44+
return EventSourceInitializer
45+
.nameEventSources(dependent.initEventSource(context));
46+
}
47+
48+
@Override
49+
public KubernetesClient getKubernetesClient() {
50+
return kubernetesClient;
51+
}
52+
53+
@Override
54+
public void setKubernetesClient(KubernetesClient kubernetesClient) {
55+
this.kubernetesClient = kubernetesClient;
56+
dependent.setKubernetesClient(kubernetesClient);
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent.external;
2+
3+
import java.util.*;
4+
import java.util.stream.Collectors;
5+
6+
import io.javaoperatorsdk.operator.api.reconciler.Context;
7+
import io.javaoperatorsdk.operator.api.reconciler.ResourceDiscriminator;
8+
import io.javaoperatorsdk.operator.processing.dependent.*;
9+
import io.javaoperatorsdk.operator.processing.dependent.external.PollingDependentResource;
10+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
11+
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;
12+
13+
public class ExternalBulkDependentResource
14+
extends PollingDependentResource<ExternalResource, BulkDependentTestCustomResource>
15+
implements BulkDependentResource<ExternalResource, BulkDependentTestCustomResource>,
16+
BulkUpdater<ExternalResource, BulkDependentTestCustomResource> {
17+
18+
public static final String EXTERNAL_RESOURCE_NAME_DELIMITER = "#";
19+
20+
private final ExternalServiceMock externalServiceMock = ExternalServiceMock.getInstance();
21+
22+
public ExternalBulkDependentResource() {
23+
super(ExternalResource.class, ExternalResource::getId);
24+
}
25+
26+
@Override
27+
public Map<ResourceID, Set<ExternalResource>> fetchResources() {
28+
Map<ResourceID, Set<ExternalResource>> result = new HashMap<>();
29+
var resources = externalServiceMock.listResources();
30+
resources.forEach(er -> {
31+
var resourceID = toResourceID(er);
32+
result.putIfAbsent(resourceID, new HashSet<>());
33+
result.get(resourceID).add(er);
34+
});
35+
return result;
36+
}
37+
38+
@Override
39+
public void delete(BulkDependentTestCustomResource primary,
40+
Context<BulkDependentTestCustomResource> context) {
41+
deleteBulkResourcesIfRequired(0, lastKnownBulkSize(), primary, context);
42+
}
43+
44+
@Override
45+
public int count(BulkDependentTestCustomResource primary,
46+
Context<BulkDependentTestCustomResource> context) {
47+
return primary.getSpec().getNumberOfResources();
48+
}
49+
50+
@Override
51+
public void deleteBulkResourceWithIndex(BulkDependentTestCustomResource primary,
52+
ExternalResource resource, int i, Context<BulkDependentTestCustomResource> context) {
53+
externalServiceMock.delete(resource.getId());
54+
}
55+
56+
@Override
57+
public ExternalResource desired(BulkDependentTestCustomResource primary, int index,
58+
Context<BulkDependentTestCustomResource> context) {
59+
return new ExternalResource(toExternalResourceId(primary, index),
60+
primary.getSpec().getAdditionalData());
61+
}
62+
63+
@Override
64+
public ExternalResource create(ExternalResource desired, BulkDependentTestCustomResource primary,
65+
Context<BulkDependentTestCustomResource> context) {
66+
return externalServiceMock.create(desired);
67+
}
68+
69+
@Override
70+
public ExternalResource update(ExternalResource actual, ExternalResource desired,
71+
BulkDependentTestCustomResource primary, Context<BulkDependentTestCustomResource> context) {
72+
return externalServiceMock.update(desired);
73+
}
74+
75+
@Override
76+
public Matcher.Result<ExternalResource> match(ExternalResource actualResource,
77+
BulkDependentTestCustomResource primary,
78+
int index, Context<BulkDependentTestCustomResource> context) {
79+
var desired = desired(primary, index, context);
80+
return Matcher.Result.computed(desired.equals(actualResource), desired);
81+
}
82+
83+
private static String toExternalResourceId(BulkDependentTestCustomResource primary, int i) {
84+
return primary.getMetadata().getName() + EXTERNAL_RESOURCE_NAME_DELIMITER +
85+
primary.getMetadata().getNamespace() +
86+
EXTERNAL_RESOURCE_NAME_DELIMITER + i;
87+
}
88+
89+
private ResourceID toResourceID(ExternalResource externalResource) {
90+
var parts = externalResource.getId().split(EXTERNAL_RESOURCE_NAME_DELIMITER);
91+
return new ResourceID(parts[0], parts[1]);
92+
}
93+
94+
@Override
95+
public ResourceDiscriminator<ExternalResource, BulkDependentTestCustomResource> getResourceDiscriminator(
96+
int index) {
97+
return (resource, primary, context) -> context.getSecondaryResources(resource).stream()
98+
.filter(r -> r.getId().endsWith(EXTERNAL_RESOURCE_NAME_DELIMITER + index))
99+
.collect(Collectors.toList()).stream().findFirst();
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent.external;
2+
3+
import io.javaoperatorsdk.operator.api.reconciler.Context;
4+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
5+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
6+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
7+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
8+
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;
9+
10+
@ControllerConfiguration(dependents = @Dependent(type = ExternalBulkDependentResource.class))
11+
public class ExternalBulkResourceReconciler implements Reconciler<BulkDependentTestCustomResource> {
12+
13+
@Override
14+
public UpdateControl<BulkDependentTestCustomResource> reconcile(
15+
BulkDependentTestCustomResource resource, Context<BulkDependentTestCustomResource> context)
16+
throws Exception {
17+
return UpdateControl.noUpdate();
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent.external;
2+
3+
import java.util.Objects;
4+
5+
public class ExternalResource {
6+
7+
private String id;
8+
private String data;
9+
10+
public ExternalResource(String id, String data) {
11+
this.id = id;
12+
this.data = data;
13+
}
14+
15+
public String getId() {
16+
return id;
17+
}
18+
19+
public String getData() {
20+
return data;
21+
}
22+
23+
@Override
24+
public boolean equals(Object o) {
25+
if (this == o)
26+
return true;
27+
if (o == null || getClass() != o.getClass())
28+
return false;
29+
ExternalResource that = (ExternalResource) o;
30+
return Objects.equals(id, that.id) && Objects.equals(data, that.data);
31+
}
32+
33+
@Override
34+
public int hashCode() {
35+
return Objects.hash(id, data);
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent.external;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Map;
6+
import java.util.Optional;
7+
import java.util.concurrent.ConcurrentHashMap;
8+
9+
public class ExternalServiceMock {
10+
11+
private static ExternalServiceMock serviceMock = new ExternalServiceMock();
12+
13+
private Map<String, ExternalResource> resourceMap = new ConcurrentHashMap<>();
14+
15+
public ExternalResource create(ExternalResource externalResource) {
16+
resourceMap.put(externalResource.getId(), externalResource);
17+
return externalResource;
18+
}
19+
20+
public Optional<ExternalResource> read(String id) {
21+
return Optional.ofNullable(resourceMap.get(id));
22+
}
23+
24+
public ExternalResource update(ExternalResource externalResource) {
25+
return resourceMap.put(externalResource.getId(), externalResource);
26+
}
27+
28+
public Optional<ExternalResource> delete(String id) {
29+
return Optional.ofNullable(resourceMap.remove(id));
30+
}
31+
32+
public List<ExternalResource> listResources() {
33+
return new ArrayList<>(resourceMap.values());
34+
}
35+
36+
public static ExternalServiceMock getInstance() {
37+
return serviceMock;
38+
}
39+
}

‎operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentresource/MultipleDependentResourceReconciler.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public class MultipleDependentResourceReconciler
2929

3030
public MultipleDependentResourceReconciler() {
3131
firstDependentResourceConfigMap = new MultipleDependentResourceConfigMap(FIRST_CONFIG_MAP_ID);
32-
3332
secondDependentResourceConfigMap = new MultipleDependentResourceConfigMap(SECOND_CONFIG_MAP_ID);
3433

3534
firstDependentResourceConfigMap

0 commit comments

Comments
 (0)
Please sign in to comment.