Skip to content

Commit 1044018

Browse files
committed
feat: read-only bulk dependent (#2372)
Signed-off-by: Attila Mészáros <csviri@gmail.com>
1 parent 0cd5a92 commit 1044018

File tree

11 files changed

+247
-43
lines changed

11 files changed

+247
-43
lines changed

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public ReconcileResult<R> reconcile(P primary, Context<P> context) {
6060
}
6161

6262
protected ReconcileResult<R> reconcile(P primary, R actualResource, Context<P> context) {
63-
if (creatable || updatable) {
63+
if (creatable() || updatable()) {
6464
if (actualResource == null) {
6565
if (creatable) {
6666
var desired = desired(primary, context);
@@ -70,7 +70,7 @@ protected ReconcileResult<R> reconcile(P primary, R actualResource, Context<P> c
7070
return ReconcileResult.resourceCreated(createdResource);
7171
}
7272
} else {
73-
if (updatable) {
73+
if (updatable()) {
7474
final Matcher.Result<R> match = match(actualResource, primary, context);
7575
if (!match.matched()) {
7676
final var desired = match.computedDesired().orElseGet(() -> desired(primary, context));
@@ -223,4 +223,12 @@ public String name() {
223223
public void setName(String name) {
224224
this.name = name;
225225
}
226+
227+
protected boolean creatable() {
228+
return creatable;
229+
}
230+
231+
protected boolean updatable() {
232+
return updatable;
233+
}
226234
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@
1313
* {@link Creator} and {@link Deleter} interfaces out of the box. A concrete dependent resource can
1414
* implement additionally also {@link Updater}.
1515
*/
16-
public interface BulkDependentResource<R, P extends HasMetadata>
17-
extends Creator<R, P>, Deleter<P> {
16+
public interface BulkDependentResource<R, P extends HasMetadata> {
1817

1918
/**
2019
* Retrieves a map of desired secondary resources associated with the specified primary resource,
@@ -26,7 +25,10 @@ public interface BulkDependentResource<R, P extends HasMetadata>
2625
* @return a Map associating desired secondary resources with the specified primary via arbitrary
2726
* identifiers
2827
*/
29-
Map<String, R> desiredResources(P primary, Context<P> context);
28+
default Map<String, R> desiredResources(P primary, Context<P> context) {
29+
throw new IllegalStateException(
30+
"Implement desiredResources in case a non read-only bulk dependent resource");
31+
}
3032

3133
/**
3234
* Retrieves the actual secondary resources currently existing on the server and associated with

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

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,26 @@ class BulkDependentResourceReconciler<R, P extends HasMetadata>
2424

2525
@Override
2626
public ReconcileResult<R> reconcile(P primary, Context<P> context) {
27-
final var desiredResources = bulkDependentResource.desiredResources(primary, context);
27+
2828
Map<String, R> actualResources = bulkDependentResource.getSecondaryResources(primary, context);
29+
if (!(bulkDependentResource instanceof Creator<?, ?>)
30+
&& !(bulkDependentResource instanceof Deleter<?>)
31+
&& !(bulkDependentResource instanceof Updater<?, ?>)) {
32+
return ReconcileResult
33+
.aggregatedResult(actualResources.values().stream().map(ReconcileResult::noOperation)
34+
.toList());
35+
}
2936

30-
// remove existing resources that are not needed anymore according to the primary state
31-
deleteExtraResources(desiredResources.keySet(), actualResources, primary, context);
37+
final var desiredResources = bulkDependentResource.desiredResources(primary, context);
38+
39+
if (bulkDependentResource instanceof Deleter<?>) {
40+
// remove existing resources that are not needed anymore according to the primary state
41+
deleteExtraResources(desiredResources.keySet(), actualResources, primary, context);
42+
}
3243

3344
final List<ReconcileResult<R>> results = new ArrayList<>(desiredResources.size());
34-
final var updatable = bulkDependentResource instanceof Updater;
3545
desiredResources.forEach((key, value) -> {
36-
final var instance =
37-
updatable ? new UpdatableBulkDependentResourceInstance<>(bulkDependentResource, value)
38-
: new BulkDependentResourceInstance<>(bulkDependentResource, value);
46+
final var instance = new BulkDependentResourceInstance<>(bulkDependentResource, value);
3947
results.add(instance.reconcile(primary, actualResources.get(key), context));
4048
});
4149

@@ -67,7 +75,7 @@ private void deleteExtraResources(Set<String> expectedKeys,
6775
@Ignore
6876
private static class BulkDependentResourceInstance<R, P extends HasMetadata>
6977
extends AbstractDependentResource<R, P>
70-
implements Creator<R, P>, Deleter<P> {
78+
implements Creator<R, P>, Deleter<P>, Updater<R, P> {
7179
private final BulkDependentResource<R, P> bulkDependentResource;
7280
private final R desired;
7381

@@ -112,26 +120,24 @@ public Class<R> resourceType() {
112120
return asAbstractDependentResource().resourceType();
113121
}
114122

115-
@Override
123+
@SuppressWarnings("unchecked")
116124
public R create(R desired, P primary, Context<P> context) {
117-
return bulkDependentResource.create(desired, primary, context);
125+
return ((Creator<R, P>) bulkDependentResource).create(desired, primary, context);
118126
}
119-
}
120127

121-
/**
122-
* Makes sure that the instance implements Updater if its precursor does as well.
123-
*
124-
* @param <R>
125-
* @param <P>
126-
*/
127-
@Ignore
128-
private static class UpdatableBulkDependentResourceInstance<R, P extends HasMetadata>
129-
extends BulkDependentResourceInstance<R, P> implements Updater<R, P> {
128+
@Override
129+
protected boolean isCreatable() {
130+
return bulkDependentResource instanceof Creator<?, ?>;
131+
}
130132

131-
private UpdatableBulkDependentResourceInstance(
132-
BulkDependentResource<R, P> bulkDependentResource,
133-
R desired) {
134-
super(bulkDependentResource, desired);
133+
@Override
134+
protected boolean isUpdatable() {
135+
return bulkDependentResource instanceof Updater<?, ?>;
136+
}
137+
138+
@Override
139+
public boolean isDeletable() {
140+
return bulkDependentResource instanceof Deleter<?>;
135141
}
136142
}
137143
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.javaoperatorsdk.operator.processing.dependent;
2+
3+
import io.fabric8.kubernetes.api.model.HasMetadata;
4+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
5+
6+
public interface CRUDBulkDependentResource<R, P extends HasMetadata>
7+
extends BulkDependentResource<R, P>,
8+
Creator<R, P>,
9+
BulkUpdater<R, P>,
10+
Deleter<P> {
11+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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.bulkdependent.BulkDependentTestCustomResource;
12+
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestSpec;
13+
import io.javaoperatorsdk.operator.sample.bulkdependent.readonly.ReadOnlyBulkDependentResource;
14+
import io.javaoperatorsdk.operator.sample.bulkdependent.readonly.ReadOnlyBulkReconciler;
15+
16+
import static org.assertj.core.api.Assertions.assertThat;
17+
import static org.awaitility.Awaitility.await;
18+
19+
public class ReadOnlyBulkDependentIT {
20+
21+
public static final int EXPECTED_NUMBER_OF_RESOURCES = 2;
22+
public static final String TEST = "test";
23+
24+
@RegisterExtension
25+
LocallyRunOperatorExtension extension =
26+
LocallyRunOperatorExtension.builder()
27+
.withReconciler(new ReadOnlyBulkReconciler())
28+
.build();
29+
30+
@Test
31+
void readOnlyBulkDependent() {
32+
var primary = extension.create(testCustomResource());
33+
34+
await().pollDelay(Duration.ofMillis(150)).untilAsserted(() -> {
35+
var actualPrimary = extension.get(BulkDependentTestCustomResource.class, TEST);
36+
37+
assertThat(actualPrimary.getStatus()).isNotNull();
38+
assertThat(actualPrimary.getStatus().getReady()).isFalse();
39+
});
40+
41+
var configMap1 = createConfigMap(1, primary);
42+
extension.create(configMap1);
43+
var configMap2 = createConfigMap(2, primary);
44+
extension.create(configMap2);
45+
46+
await().untilAsserted(() -> {
47+
var actualPrimary = extension.get(BulkDependentTestCustomResource.class, TEST);
48+
assertThat(actualPrimary.getStatus().getReady()).isTrue();
49+
});
50+
}
51+
52+
private ConfigMap createConfigMap(int i, BulkDependentTestCustomResource primary) {
53+
ConfigMap configMap = new ConfigMap();
54+
configMap.setMetadata(new ObjectMetaBuilder()
55+
.withName(TEST + ReadOnlyBulkDependentResource.INDEX_DELIMITER + i)
56+
.withNamespace(primary.getMetadata().getNamespace())
57+
.build());
58+
configMap.addOwnerReference(primary);
59+
return configMap;
60+
}
61+
62+
BulkDependentTestCustomResource testCustomResource() {
63+
BulkDependentTestCustomResource customResource = new BulkDependentTestCustomResource();
64+
customResource.setMetadata(new ObjectMetaBuilder()
65+
.withName(TEST)
66+
.build());
67+
customResource.setSpec(new BulkDependentTestSpec());
68+
customResource.getSpec().setNumberOfResources(EXPECTED_NUMBER_OF_RESOURCES);
69+
70+
return customResource;
71+
}
72+
73+
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ConfigMapDeleterBulkDependentResource.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,7 @@
88
import io.fabric8.kubernetes.api.model.ConfigMap;
99
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
1010
import io.javaoperatorsdk.operator.api.reconciler.Context;
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;
11+
import io.javaoperatorsdk.operator.processing.dependent.*;
1512
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
1613

1714
/**
@@ -20,10 +17,7 @@
2017
public class ConfigMapDeleterBulkDependentResource
2118
extends
2219
KubernetesDependentResource<ConfigMap, BulkDependentTestCustomResource>
23-
implements Creator<ConfigMap, BulkDependentTestCustomResource>,
24-
Updater<ConfigMap, BulkDependentTestCustomResource>,
25-
Deleter<BulkDependentTestCustomResource>,
26-
BulkDependentResource<ConfigMap, BulkDependentTestCustomResource> {
20+
implements CRUDBulkDependentResource<ConfigMap, BulkDependentTestCustomResource> {
2721

2822
public static final String LABEL_KEY = "bulk";
2923
public static final String LABEL_VALUE = "true";

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalBulkDependentResource.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@
77
import java.util.stream.Collectors;
88

99
import io.javaoperatorsdk.operator.api.reconciler.Context;
10+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
1011
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
1112
import io.javaoperatorsdk.operator.processing.dependent.BulkUpdater;
13+
import io.javaoperatorsdk.operator.processing.dependent.Creator;
1214
import io.javaoperatorsdk.operator.processing.dependent.external.PollingDependentResource;
1315
import io.javaoperatorsdk.operator.processing.event.ResourceID;
1416
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;
1517

1618
public class ExternalBulkDependentResource
1719
extends PollingDependentResource<ExternalResource, BulkDependentTestCustomResource>
1820
implements BulkDependentResource<ExternalResource, BulkDependentTestCustomResource>,
21+
Creator<ExternalResource, BulkDependentTestCustomResource>,
22+
Deleter<BulkDependentTestCustomResource>,
1923
BulkUpdater<ExternalResource, BulkDependentTestCustomResource> {
2024

2125
public static final String EXTERNAL_RESOURCE_NAME_DELIMITER = "#";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent.readonly;
2+
3+
import java.util.Map;
4+
import java.util.Set;
5+
import java.util.function.Function;
6+
import java.util.stream.Collectors;
7+
8+
import io.fabric8.kubernetes.api.model.ConfigMap;
9+
import io.javaoperatorsdk.operator.api.reconciler.Context;
10+
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
11+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
12+
import io.javaoperatorsdk.operator.processing.event.ResourceID;
13+
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
14+
import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers;
15+
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;
16+
17+
18+
public class ReadOnlyBulkDependentResource
19+
extends
20+
KubernetesDependentResource<ConfigMap, BulkDependentTestCustomResource>
21+
implements BulkDependentResource<ConfigMap, BulkDependentTestCustomResource>,
22+
SecondaryToPrimaryMapper<ConfigMap> {
23+
24+
public static final String INDEX_DELIMITER = "-";
25+
26+
public ReadOnlyBulkDependentResource() {
27+
super(ConfigMap.class);
28+
}
29+
30+
@Override
31+
protected Class<BulkDependentTestCustomResource> getPrimaryResourceType() {
32+
return BulkDependentTestCustomResource.class;
33+
}
34+
35+
@Override
36+
public Map<String, ConfigMap> getSecondaryResources(BulkDependentTestCustomResource primary,
37+
Context<BulkDependentTestCustomResource> context) {
38+
return context.getSecondaryResourcesAsStream(ConfigMap.class)
39+
.filter(cm -> getName(cm).startsWith(primary.getMetadata().getName()))
40+
.collect(Collectors.toMap(
41+
cm -> getName(cm).substring(getName(cm).lastIndexOf(INDEX_DELIMITER) + 1),
42+
Function.identity()));
43+
}
44+
45+
private static String getName(ConfigMap cm) {
46+
return cm.getMetadata().getName();
47+
}
48+
49+
@Override
50+
public Set<ResourceID> toPrimaryResourceIDs(ConfigMap resource) {
51+
return Mappers.fromOwnerReferences(false).toPrimaryResourceIDs(resource);
52+
}
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent.readonly;
2+
3+
import io.fabric8.kubernetes.api.model.ConfigMap;
4+
import io.javaoperatorsdk.operator.api.reconciler.Context;
5+
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
6+
import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource;
7+
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
8+
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;
9+
10+
public class ReadOnlyBulkReadyPostCondition
11+
implements Condition<ConfigMap, BulkDependentTestCustomResource> {
12+
@Override
13+
public boolean isMet(
14+
DependentResource<ConfigMap, BulkDependentTestCustomResource> dependentResource,
15+
BulkDependentTestCustomResource primary, Context<BulkDependentTestCustomResource> context) {
16+
var minResourceNumber = primary.getSpec().getNumberOfResources();
17+
@SuppressWarnings("unchecked")
18+
var secondaryResources =
19+
((BulkDependentResource<ConfigMap, BulkDependentTestCustomResource>) dependentResource)
20+
.getSecondaryResources(primary, context);
21+
return minResourceNumber <= secondaryResources.size();
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.javaoperatorsdk.operator.sample.bulkdependent.readonly;
2+
3+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
4+
import io.javaoperatorsdk.operator.api.reconciler.*;
5+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
6+
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource;
7+
import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestStatus;
8+
9+
@Workflow(dependents = @Dependent(type = ReadOnlyBulkDependentResource.class,
10+
readyPostcondition = ReadOnlyBulkReadyPostCondition.class))
11+
@ControllerConfiguration
12+
public class ReadOnlyBulkReconciler implements Reconciler<BulkDependentTestCustomResource> {
13+
@Override
14+
public UpdateControl<BulkDependentTestCustomResource> reconcile(
15+
BulkDependentTestCustomResource resource, Context<BulkDependentTestCustomResource> context) {
16+
17+
var nonReadyDependents =
18+
context.managedWorkflowAndDependentResourceContext().getWorkflowReconcileResult()
19+
.getNotReadyDependents();
20+
21+
22+
BulkDependentTestCustomResource customResource = new BulkDependentTestCustomResource();
23+
customResource.setMetadata(new ObjectMetaBuilder()
24+
.withName(resource.getMetadata().getName())
25+
.withNamespace(resource.getMetadata().getNamespace())
26+
.build());
27+
var status = new BulkDependentTestStatus();
28+
status.setReady(nonReadyDependents.isEmpty());
29+
customResource.setStatus(status);
30+
31+
return UpdateControl.patchStatus(customResource);
32+
}
33+
}

0 commit comments

Comments
 (0)