Skip to content

Commit 34c71c3

Browse files
authored
feat: garbage collected interface (#1164)
1 parent 4dfec23 commit 34c71c3

File tree

23 files changed

+307
-78
lines changed

23 files changed

+307
-78
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.javaoperatorsdk.operator.api.reconciler.dependent;
2+
3+
import io.fabric8.kubernetes.api.model.HasMetadata;
4+
5+
/**
6+
* Should be implemented by {@link DependentResource} implementations that are explicitly deleted
7+
* during reconciliation but which should also benefit from Kubernetes' automated garbage collection
8+
* during the cleanup phase.
9+
* <p>
10+
* See <a href="https://github.com/java-operator-sdk/java-operator-sdk/issues/1127">this issue</a>
11+
* for more details.
12+
*/
13+
public interface GarbageCollected<P extends HasMetadata> extends Deleter<P> {
14+
15+
}

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
3838
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
3939
import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider;
40+
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
4041
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator;
4142
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware;
4243
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceException;
@@ -74,15 +75,16 @@ public Controller(Reconciler<P> reconciler,
7475

7576
final var hasDeleterHolder = new boolean[] {false};
7677
final var specs = configuration.getDependentResources();
77-
final var size = specs.size();
78-
if (size == 0) {
78+
final var specsSize = specs.size();
79+
if (specsSize == 0) {
7980
dependents = new LinkedHashMap<>();
8081
} else {
81-
final Map<String, DependentResource> dependentsHolder = new LinkedHashMap<>(size);
82+
final Map<String, DependentResource> dependentsHolder = new LinkedHashMap<>(specsSize);
8283
specs.forEach(drs -> {
8384
final var dependent = createAndConfigureFrom(drs, kubernetesClient);
8485
// check if dependent implements Deleter to record that fact
85-
if (!hasDeleterHolder[0] && dependent instanceof Deleter) {
86+
if (!hasDeleterHolder[0] && dependent instanceof Deleter
87+
&& !(dependent instanceof GarbageCollected)) {
8688
hasDeleterHolder[0] = true;
8789
}
8890
dependentsHolder.put(drs.getName(), dependent);
@@ -143,7 +145,7 @@ public DeleteControl execute() {
143145
initContextIfNeeded(resource, context);
144146
if (hasDeleterDependents) {
145147
dependents.values().stream()
146-
.filter(d -> d instanceof Deleter)
148+
.filter(d -> d instanceof Deleter && !(d instanceof GarbageCollected))
147149
.map(Deleter.class::cast)
148150
.forEach(deleter -> deleter.delete(resource, context));
149151
}

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import io.fabric8.kubernetes.api.model.HasMetadata;
77
import io.javaoperatorsdk.operator.api.reconciler.Context;
8-
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
98
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
109
import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult;
1110
import io.javaoperatorsdk.operator.processing.event.ResourceID;
@@ -16,7 +15,7 @@ public abstract class AbstractDependentResource<R, P extends HasMetadata>
1615

1716
protected final boolean creatable = this instanceof Creator;
1817
protected final boolean updatable = this instanceof Updater;
19-
protected final boolean deletable = this instanceof Deleter;
18+
2019
protected Creator<R, P> creator;
2120
protected Updater<R, P> updater;
2221

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

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;
22

33
import io.fabric8.kubernetes.api.model.HasMetadata;
4-
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
4+
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
55
import io.javaoperatorsdk.operator.processing.dependent.Creator;
66
import io.javaoperatorsdk.operator.processing.dependent.Updater;
77

88
/**
9-
* Adaptor Class for standalone mode for resources that manages Create, Read, Update and Delete
9+
* Adaptor class for standalone mode for resources that manage Create, Read and Update operations
10+
* and that should be automatically garbage-collected by Kubernetes when the associated primary
11+
* resource is destroyed.
1012
*
11-
* @param <R> Managed resource
12-
* @param <P> Primary Resource
13+
* @param <R> the type of the managed dependent resource
14+
* @param <P> the type of the associated primary resource
1315
*/
1416
public abstract class CRUDKubernetesDependentResource<R extends HasMetadata, P extends HasMetadata>
1517
extends
16-
KubernetesDependentResource<R, P> implements Creator<R, P>, Updater<R, P>, Deleter<P> {
18+
KubernetesDependentResource<R, P>
19+
implements Creator<R, P>, Updater<R, P>, GarbageCollected<P> {
1720

1821
public CRUDKubernetesDependentResource(Class<R> resourceType) {
1922
super(resourceType);

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

-21
This file was deleted.

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

+7-11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
1515
import io.javaoperatorsdk.operator.api.reconciler.Context;
1616
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
17+
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
1718
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator;
1819
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware;
1920
import io.javaoperatorsdk.operator.processing.dependent.AbstractEventSourceHolderDependentResource;
@@ -35,6 +36,7 @@ public abstract class KubernetesDependentResource<R extends HasMetadata, P exten
3536
private final Matcher<R, P> matcher;
3637
private final ResourceUpdatePreProcessor<R> processor;
3738
private final Class<R> resourceType;
39+
private final boolean garbageCollected = this instanceof GarbageCollected;
3840

3941
@SuppressWarnings("unchecked")
4042
public KubernetesDependentResource(Class<R> resourceType) {
@@ -78,11 +80,9 @@ public void configureWith(InformerEventSource<R, P> informerEventSource) {
7880

7981
protected R handleCreate(R desired, P primary, Context<P> context) {
8082
ResourceID resourceID = ResourceID.fromResource(desired);
81-
R created = null;
8283
try {
8384
prepareEventFiltering(desired, resourceID);
84-
created = super.handleCreate(desired, primary, context);
85-
return created;
85+
return super.handleCreate(desired, primary, context);
8686
} catch (RuntimeException e) {
8787
cleanupAfterEventFiltering(resourceID);
8888
throw e;
@@ -91,11 +91,9 @@ protected R handleCreate(R desired, P primary, Context<P> context) {
9191

9292
protected R handleUpdate(R actual, R desired, P primary, Context<P> context) {
9393
ResourceID resourceID = ResourceID.fromResource(desired);
94-
R updated = null;
9594
try {
9695
prepareEventFiltering(desired, resourceID);
97-
updated = super.handleUpdate(actual, desired, primary, context);
98-
return updated;
96+
return super.handleUpdate(actual, desired, primary, context);
9997
} catch (RuntimeException e) {
10098
cleanupAfterEventFiltering(resourceID);
10199
throw e;
@@ -117,10 +115,8 @@ public Result<R> match(R actualResource, P primary, Context<P> context) {
117115
}
118116

119117
public void delete(P primary, Context<P> context) {
120-
if (!addOwnerReference()) {
121-
var resource = getSecondaryResource(primary);
122-
resource.ifPresent(r -> client.resource(r).delete());
123-
}
118+
var resource = getSecondaryResource(primary);
119+
resource.ifPresent(r -> client.resource(r).delete());
124120
}
125121

126122
@SuppressWarnings("unchecked")
@@ -146,7 +142,7 @@ protected InformerEventSource<R, P> createEventSource(EventSourceContext<P> cont
146142
}
147143

148144
protected boolean addOwnerReference() {
149-
return creatable && !deletable;
145+
return garbageCollected;
150146
}
151147

152148
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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.OperatorExtension;
11+
import io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection.DependentGarbageCollectionTestCustomResource;
12+
import io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection.DependentGarbageCollectionTestCustomResourceSpec;
13+
import io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection.DependentGarbageCollectionTestReconciler;
14+
15+
import static org.assertj.core.api.Assertions.assertThat;
16+
import static org.awaitility.Awaitility.await;
17+
18+
class KubernetesDependentGarbageCollectionIT {
19+
20+
public static final String TEST_RESOURCE_NAME = "test1";
21+
@RegisterExtension
22+
OperatorExtension operator =
23+
OperatorExtension.builder()
24+
.withReconciler(new DependentGarbageCollectionTestReconciler())
25+
.build();
26+
27+
28+
@Test
29+
void resourceSecondaryResourceIsGarbageCollected() {
30+
var resource = customResource();
31+
var createdResources =
32+
operator.create(DependentGarbageCollectionTestCustomResource.class, resource);
33+
34+
await().untilAsserted(() -> {
35+
ConfigMap configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
36+
assertThat(configMap).isNotNull();
37+
});
38+
39+
ConfigMap configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
40+
assertThat(configMap.getMetadata().getOwnerReferences()).hasSize(1);
41+
assertThat(configMap.getMetadata().getOwnerReferences().get(0).getName())
42+
.isEqualTo(TEST_RESOURCE_NAME);
43+
44+
operator.delete(DependentGarbageCollectionTestCustomResource.class, createdResources);
45+
46+
await().atMost(Duration.ofSeconds(30)).untilAsserted(() -> {
47+
ConfigMap cm = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
48+
assertThat(cm).isNull();
49+
});
50+
}
51+
52+
@Test
53+
void deletesSecondaryResource() {
54+
var resource = customResource();
55+
var createdResources =
56+
operator.create(DependentGarbageCollectionTestCustomResource.class, resource);
57+
58+
await().untilAsserted(() -> {
59+
ConfigMap configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
60+
assertThat(configMap).isNotNull();
61+
});
62+
63+
createdResources.getSpec().setCreateConfigMap(false);
64+
operator.replace(DependentGarbageCollectionTestCustomResource.class, createdResources);
65+
66+
await().untilAsserted(() -> {
67+
ConfigMap cm = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
68+
assertThat(cm).isNull();
69+
});
70+
}
71+
72+
DependentGarbageCollectionTestCustomResource customResource() {
73+
DependentGarbageCollectionTestCustomResource resource =
74+
new DependentGarbageCollectionTestCustomResource();
75+
resource.setMetadata(new ObjectMetaBuilder()
76+
.withName(TEST_RESOURCE_NAME)
77+
.build());
78+
resource.setSpec(new DependentGarbageCollectionTestCustomResourceSpec());
79+
resource.getSpec().setCreateConfigMap(true);
80+
return resource;
81+
}
82+
83+
}

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/cleanermanageddependent/ConfigMapDependentResource.java

+8-2
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@
66
import io.fabric8.kubernetes.api.model.ConfigMap;
77
import io.fabric8.kubernetes.api.model.ObjectMeta;
88
import io.javaoperatorsdk.operator.api.reconciler.Context;
9-
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
9+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
10+
import io.javaoperatorsdk.operator.processing.dependent.Creator;
11+
import io.javaoperatorsdk.operator.processing.dependent.Updater;
12+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;
1013

1114
public class ConfigMapDependentResource extends
12-
CRUDKubernetesDependentResource<ConfigMap, CleanerForManagedDependentCustomResource> {
15+
KubernetesDependentResource<ConfigMap, CleanerForManagedDependentCustomResource>
16+
implements Creator<ConfigMap, CleanerForManagedDependentCustomResource>,
17+
Updater<ConfigMap, CleanerForManagedDependentCustomResource>,
18+
Deleter<CleanerForManagedDependentCustomResource> {
1319

1420
private static final AtomicInteger numberOfCleanupExecutions = new AtomicInteger(0);
1521

operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentoperationeventfiltering/ConfigMapDependentResource.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
import io.fabric8.kubernetes.api.model.ConfigMap;
66
import io.fabric8.kubernetes.api.model.ObjectMeta;
77
import io.javaoperatorsdk.operator.api.reconciler.Context;
8-
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource;
8+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
99

1010
public class ConfigMapDependentResource extends
11-
CRUKubernetesDependentResource<ConfigMap, DependentOperationEventFilterCustomResource> {
11+
CRUDKubernetesDependentResource<ConfigMap, DependentOperationEventFilterCustomResource> {
1212

1313
public static final String KEY = "key1";
1414

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection;
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("dgc")
12+
public class DependentGarbageCollectionTestCustomResource
13+
extends
14+
CustomResource<DependentGarbageCollectionTestCustomResourceSpec, DependentGarbageCollectionTestCustomResourceStatus>
15+
implements Namespaced {
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection;
2+
3+
public class DependentGarbageCollectionTestCustomResourceSpec {
4+
5+
private boolean createConfigMap;
6+
7+
public boolean isCreateConfigMap() {
8+
return createConfigMap;
9+
}
10+
11+
public DependentGarbageCollectionTestCustomResourceSpec setCreateConfigMap(
12+
boolean createConfigMap) {
13+
this.createConfigMap = createConfigMap;
14+
return this;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection;
2+
3+
public class DependentGarbageCollectionTestCustomResourceStatus {
4+
5+
}

0 commit comments

Comments
 (0)