Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.javaoperatorsdk.operator.api.reconciler.dependent;

import io.fabric8.kubernetes.api.model.HasMetadata;

/**
* Should be implemented by {@link DependentResource} implementations that are explicitly deleted
* during reconciliation but which should also benefit from Kubernetes' automated garbage collection
* during the cleanup phase.
* <p>
* See <a href="https://github.com/java-operator-sdk/java-operator-sdk/issues/1127">this issue</a>
* for more details.
*/
public interface GarbageCollected<P extends HasMetadata> extends Deleter<P> {

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider;
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceException;
Expand Down Expand Up @@ -74,15 +75,16 @@ public Controller(Reconciler<P> reconciler,

final var hasDeleterHolder = new boolean[] {false};
final var specs = configuration.getDependentResources();
final var size = specs.size();
if (size == 0) {
final var specsSize = specs.size();
if (specsSize == 0) {
dependents = new LinkedHashMap<>();
} else {
final Map<String, DependentResource> dependentsHolder = new LinkedHashMap<>(size);
final Map<String, DependentResource> dependentsHolder = new LinkedHashMap<>(specsSize);
specs.forEach(drs -> {
final var dependent = createAndConfigureFrom(drs, kubernetesClient);
// check if dependent implements Deleter to record that fact
if (!hasDeleterHolder[0] && dependent instanceof Deleter) {
if (!hasDeleterHolder[0] && dependent instanceof Deleter
&& !(dependent instanceof GarbageCollected)) {
hasDeleterHolder[0] = true;
}
dependentsHolder.put(drs.getName(), dependent);
Expand Down Expand Up @@ -143,7 +145,7 @@ public DeleteControl execute() {
initContextIfNeeded(resource, context);
if (hasDeleterDependents) {
dependents.values().stream()
.filter(d -> d instanceof Deleter)
.filter(d -> d instanceof Deleter && !(d instanceof GarbageCollected))
.map(Deleter.class::cast)
.forEach(deleter -> deleter.delete(resource, context));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

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

protected final boolean creatable = this instanceof Creator;
protected final boolean updatable = this instanceof Updater;
protected final boolean deletable = this instanceof Deleter;

protected Creator<R, P> creator;
protected Updater<R, P> updater;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
package io.javaoperatorsdk.operator.processing.dependent.kubernetes;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
import io.javaoperatorsdk.operator.processing.dependent.Creator;
import io.javaoperatorsdk.operator.processing.dependent.Updater;

/**
* Adaptor Class for standalone mode for resources that manages Create, Read, Update and Delete
* Adaptor class for standalone mode for resources that manage Create, Read and Update operations
* and that should be automatically garbage-collected by Kubernetes when the associated primary
* resource is destroyed.
*
* @param <R> Managed resource
* @param <P> Primary Resource
* @param <R> the type of the managed dependent resource
* @param <P> the type of the associated primary resource
*/
public abstract class CRUDKubernetesDependentResource<R extends HasMetadata, P extends HasMetadata>
extends
KubernetesDependentResource<R, P> implements Creator<R, P>, Updater<R, P>, Deleter<P> {
KubernetesDependentResource<R, P>
implements Creator<R, P>, Updater<R, P>, GarbageCollected<P> {

public CRUDKubernetesDependentResource(Class<R> resourceType) {
super(resourceType);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator;
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware;
import io.javaoperatorsdk.operator.processing.dependent.AbstractEventSourceHolderDependentResource;
Expand All @@ -35,6 +36,7 @@ public abstract class KubernetesDependentResource<R extends HasMetadata, P exten
private final Matcher<R, P> matcher;
private final ResourceUpdatePreProcessor<R> processor;
private final Class<R> resourceType;
private final boolean garbageCollected = this instanceof GarbageCollected;

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

protected R handleCreate(R desired, P primary, Context<P> context) {
ResourceID resourceID = ResourceID.fromResource(desired);
R created = null;
try {
prepareEventFiltering(desired, resourceID);
created = super.handleCreate(desired, primary, context);
return created;
return super.handleCreate(desired, primary, context);
} catch (RuntimeException e) {
cleanupAfterEventFiltering(resourceID);
throw e;
Expand All @@ -91,11 +91,9 @@ protected R handleCreate(R desired, P primary, Context<P> context) {

protected R handleUpdate(R actual, R desired, P primary, Context<P> context) {
ResourceID resourceID = ResourceID.fromResource(desired);
R updated = null;
try {
prepareEventFiltering(desired, resourceID);
updated = super.handleUpdate(actual, desired, primary, context);
return updated;
return super.handleUpdate(actual, desired, primary, context);
} catch (RuntimeException e) {
cleanupAfterEventFiltering(resourceID);
throw e;
Expand All @@ -117,10 +115,8 @@ public Result<R> match(R actualResource, P primary, Context<P> context) {
}

public void delete(P primary, Context<P> context) {
if (!addOwnerReference()) {
var resource = getSecondaryResource(primary);
resource.ifPresent(r -> client.resource(r).delete());
}
var resource = getSecondaryResource(primary);
resource.ifPresent(r -> client.resource(r).delete());
}

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

protected boolean addOwnerReference() {
return creatable && !deletable;
return garbageCollected;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package io.javaoperatorsdk.operator;

import java.time.Duration;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
import io.javaoperatorsdk.operator.junit.OperatorExtension;
import io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection.DependentGarbageCollectionTestCustomResource;
import io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection.DependentGarbageCollectionTestCustomResourceSpec;
import io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection.DependentGarbageCollectionTestReconciler;

import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;

class KubernetesDependentGarbageCollectionIT {

public static final String TEST_RESOURCE_NAME = "test1";
@RegisterExtension
OperatorExtension operator =
OperatorExtension.builder()
.withReconciler(new DependentGarbageCollectionTestReconciler())
.build();


@Test
void resourceSecondaryResourceIsGarbageCollected() {
var resource = customResource();
var createdResources =
operator.create(DependentGarbageCollectionTestCustomResource.class, resource);

await().untilAsserted(() -> {
ConfigMap configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
assertThat(configMap).isNotNull();
});

ConfigMap configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
assertThat(configMap.getMetadata().getOwnerReferences()).hasSize(1);
assertThat(configMap.getMetadata().getOwnerReferences().get(0).getName())
.isEqualTo(TEST_RESOURCE_NAME);

operator.delete(DependentGarbageCollectionTestCustomResource.class, createdResources);

await().atMost(Duration.ofSeconds(30)).untilAsserted(() -> {
ConfigMap cm = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
assertThat(cm).isNull();
});
}

@Test
void deletesSecondaryResource() {
var resource = customResource();
var createdResources =
operator.create(DependentGarbageCollectionTestCustomResource.class, resource);

await().untilAsserted(() -> {
ConfigMap configMap = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
assertThat(configMap).isNotNull();
});

createdResources.getSpec().setCreateConfigMap(false);
operator.replace(DependentGarbageCollectionTestCustomResource.class, createdResources);

await().untilAsserted(() -> {
ConfigMap cm = operator.get(ConfigMap.class, TEST_RESOURCE_NAME);
assertThat(cm).isNull();
});
}

DependentGarbageCollectionTestCustomResource customResource() {
DependentGarbageCollectionTestCustomResource resource =
new DependentGarbageCollectionTestCustomResource();
resource.setMetadata(new ObjectMetaBuilder()
.withName(TEST_RESOURCE_NAME)
.build());
resource.setSpec(new DependentGarbageCollectionTestCustomResourceSpec());
resource.getSpec().setCreateConfigMap(true);
return resource;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter;
import io.javaoperatorsdk.operator.processing.dependent.Creator;
import io.javaoperatorsdk.operator.processing.dependent.Updater;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource;

public class ConfigMapDependentResource extends
CRUDKubernetesDependentResource<ConfigMap, CleanerForManagedDependentCustomResource> {
KubernetesDependentResource<ConfigMap, CleanerForManagedDependentCustomResource>
implements Creator<ConfigMap, CleanerForManagedDependentCustomResource>,
Updater<ConfigMap, CleanerForManagedDependentCustomResource>,
Deleter<CleanerForManagedDependentCustomResource> {

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
import io.fabric8.kubernetes.api.model.ConfigMap;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUKubernetesDependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;

public class ConfigMapDependentResource extends
CRUKubernetesDependentResource<ConfigMap, DependentOperationEventFilterCustomResource> {
CRUDKubernetesDependentResource<ConfigMap, DependentOperationEventFilterCustomResource> {

public static final String KEY = "key1";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection;

import io.fabric8.kubernetes.api.model.Namespaced;
import io.fabric8.kubernetes.client.CustomResource;
import io.fabric8.kubernetes.model.annotation.Group;
import io.fabric8.kubernetes.model.annotation.ShortNames;
import io.fabric8.kubernetes.model.annotation.Version;

@Group("sample.javaoperatorsdk")
@Version("v1")
@ShortNames("dgc")
public class DependentGarbageCollectionTestCustomResource
extends
CustomResource<DependentGarbageCollectionTestCustomResourceSpec, DependentGarbageCollectionTestCustomResourceStatus>
implements Namespaced {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection;

public class DependentGarbageCollectionTestCustomResourceSpec {

private boolean createConfigMap;

public boolean isCreateConfigMap() {
return createConfigMap;
}

public DependentGarbageCollectionTestCustomResourceSpec setCreateConfigMap(
boolean createConfigMap) {
this.createConfigMap = createConfigMap;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.javaoperatorsdk.operator.sample.kubernetesdependentgarbagecollection;

public class DependentGarbageCollectionTestCustomResourceStatus {

}
Loading