Skip to content

feat: garbage collected interface #1164

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
May 5, 2022
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