diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java index 2c8e5af017..dbfc34fe42 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResource.java @@ -110,10 +110,9 @@ private void logForOperation(String operation, P primary, R desired) { } protected R handleCreate(R desired, P primary, Context

context) { - ResourceID resourceID = ResourceID.fromResource(primary); R created = creator.create(desired, primary, context); throwIfNull(created, primary, "Created resource"); - onCreated(resourceID, created); + onCreated(primary, created, context); return created; } @@ -121,27 +120,28 @@ protected R handleCreate(R desired, P primary, Context

context) { * Allows subclasses to perform additional processing (e.g. caching) on the created resource if * needed. * - * @param primaryResourceId the {@link ResourceID} of the primary resource associated with the - * newly created resource + * @param primary the {@link ResourceID} of the primary resource associated with the newly created + * resource * @param created the newly created resource + * @param context */ - protected abstract void onCreated(ResourceID primaryResourceId, R created); + protected abstract void onCreated(P primary, R created, Context

context); /** * Allows subclasses to perform additional processing on the updated resource if needed. * - * @param primaryResourceId the {@link ResourceID} of the primary resource associated with the - * newly updated resource + * @param primary the {@link ResourceID} of the primary resource associated with the newly updated + * resource * @param updated the updated resource * @param actual the resource as it was before the update + * @param context */ - protected abstract void onUpdated(ResourceID primaryResourceId, R updated, R actual); + protected abstract void onUpdated(P primary, R updated, R actual, Context

context); protected R handleUpdate(R actual, R desired, P primary, Context

context) { - ResourceID resourceID = ResourceID.fromResource(primary); R updated = updater.update(actual, desired, primary, context); throwIfNull(updated, primary, "Updated resource"); - onUpdated(resourceID, updated, actual); + onUpdated(primary, updated, actual, context); return updated; } @@ -154,8 +154,9 @@ public void delete(P primary, Context

context) { dependentResourceReconciler.delete(primary, context); } - protected void handleDelete(P primary, Context

context) { - throw new IllegalStateException("delete method be implemented if Deleter trait is supported"); + protected void handleDelete(P primary, R secondary, Context

context) { + throw new IllegalStateException( + "handleDelete method must be implemented if Deleter trait is supported"); } public void setResourceDiscriminator( diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java index f514a00950..ad1eeb4d43 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractEventSourceHolderDependentResource.java @@ -3,6 +3,7 @@ import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceNotFoundException; @@ -94,15 +95,17 @@ public Optional> eventSource() { return Optional.ofNullable(eventSource); } - protected void onCreated(ResourceID primaryResourceId, R created) { + protected void onCreated(P primary, R created, Context

context) { if (isCacheFillerEventSource) { - recentOperationCacheFiller().handleRecentResourceCreate(primaryResourceId, created); + recentOperationCacheFiller().handleRecentResourceCreate(ResourceID.fromResource(primary), + created); } } - protected void onUpdated(ResourceID primaryResourceId, R updated, R actual) { + protected void onUpdated(P primary, R updated, R actual, Context

context) { if (isCacheFillerEventSource) { - recentOperationCacheFiller().handleRecentResourceUpdate(primaryResourceId, updated, actual); + recentOperationCacheFiller().handleRecentResourceUpdate(ResourceID.fromResource(primary), + updated, actual); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java new file mode 100644 index 0000000000..e3dd9c630a --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/AbstractExternalDependentResource.java @@ -0,0 +1,120 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.RecentOperationCacheFiller; +import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; + +public abstract class AbstractExternalDependentResource> + extends AbstractEventSourceHolderDependentResource { + + private final boolean isDependentResourceWithExplicitState = + this instanceof DependentResourceWithExplicitState; + private final boolean isBulkDependentResource = this instanceof BulkDependentResource; + @SuppressWarnings("rawtypes") + private DependentResourceWithExplicitState dependentResourceWithExplicitState; + private InformerEventSource externalStateEventSource; + private KubernetesClient kubernetesClient; + + @SuppressWarnings("unchecked") + protected AbstractExternalDependentResource(Class resourceType) { + super(resourceType); + if (isDependentResourceWithExplicitState) { + dependentResourceWithExplicitState = (DependentResourceWithExplicitState) this; + } + } + + @Override + @SuppressWarnings("unchecked") + public void resolveEventSource(EventSourceRetriever

eventSourceRetriever) { + super.resolveEventSource(eventSourceRetriever); + if (isDependentResourceWithExplicitState) { + externalStateEventSource = (InformerEventSource) dependentResourceWithExplicitState + .eventSourceName() + .map(n -> eventSourceRetriever + .getResourceEventSourceFor(dependentResourceWithExplicitState.stateResourceClass(), + (String) n)) + .orElseGet(() -> eventSourceRetriever + .getResourceEventSourceFor( + (Class) dependentResourceWithExplicitState.stateResourceClass())); + } + + } + + @Override + protected void onCreated(P primary, R created, Context

context) { + super.onCreated(primary, created, context); + if (this instanceof DependentResourceWithExplicitState) { + handleExplicitStateCreation(primary, created, context); + } + } + + @Override + public void delete(P primary, Context

context) { + if (isDependentResourceWithExplicitState && !isBulkDependentResource) { + var secondary = getSecondaryResource(primary, context); + super.delete(primary, context); + // deletes the state after the resource is deleted + handleExplicitStateDelete(primary, secondary.orElse(null), context); + } else { + super.delete(primary, context); + } + } + + @SuppressWarnings("unchecked") + private void handleExplicitStateDelete(P primary, R secondary, Context

context) { + var res = dependentResourceWithExplicitState.stateResource(primary, secondary); + dependentResourceWithExplicitState.getKubernetesClient().resource(res).delete(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + protected void handleExplicitStateCreation(P primary, R created, Context

context) { + var resource = dependentResourceWithExplicitState.stateResource(primary, created); + var stateResource = + dependentResourceWithExplicitState.getKubernetesClient().resource(resource).create(); + if (externalStateEventSource != null) { + ((RecentOperationCacheFiller) externalStateEventSource) + .handleRecentResourceCreate(ResourceID.fromResource(primary), stateResource); + } + } + + + @SuppressWarnings("unchecked") + public void deleteTargetResource(P primary, R resource, String key, + Context

context) { + if (isDependentResourceWithExplicitState) { + getKubernetesClient() + .resource(dependentResourceWithExplicitState.stateResource(primary, resource)) + .delete(); + } + handleDeleteTargetResource(primary, resource, key, context); + } + + public void handleDeleteTargetResource(P primary, R resource, String key, + Context

context) { + throw new IllegalStateException("Override this method in case you manage an bulk resource"); + } + + @SuppressWarnings("rawtypes") + protected InformerEventSource getExternalStateEventSource() { + return externalStateEventSource; + } + + /** + * It's here just to manage the explicit state resource in case the dependent resource implements + * {@link RecentOperationCacheFiller}. + * + * @return kubernetes client. + */ + public KubernetesClient getKubernetesClient() { + return kubernetesClient; + } + + public void setKubernetesClient(KubernetesClient kubernetesClient) { + this.kubernetesClient = kubernetesClient; + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java index c6633f9db6..49b5af0ad4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResourceReconciler.java @@ -11,7 +11,6 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; import io.javaoperatorsdk.operator.processing.dependent.Matcher.Result; -import io.javaoperatorsdk.operator.processing.event.ResourceID; class BulkDependentResourceReconciler implements DependentResourceReconciler { @@ -97,13 +96,13 @@ public Result match(R resource, P primary, Context

context) { } @Override - protected void onCreated(ResourceID primaryResourceId, R created) { - asAbstractDependentResource().onCreated(primaryResourceId, created); + protected void onCreated(P primary, R created, Context

context) { + asAbstractDependentResource().onCreated(primary, created, context); } @Override - protected void onUpdated(ResourceID primaryResourceId, R updated, R actual) { - asAbstractDependentResource().onUpdated(primaryResourceId, updated, actual); + protected void onUpdated(P primary, R updated, R actual, Context

context) { + asAbstractDependentResource().onUpdated(primary, updated, actual, context); } @Override diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceWithExplicitState.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceWithExplicitState.java new file mode 100644 index 0000000000..2a1b0a64c7 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/DependentResourceWithExplicitState.java @@ -0,0 +1,37 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; + +/** + * Handles external resources where in order to address the resource additional information or + * persistent state (usually the ID of the resource) is needed to access the current state. These + * are non Kubernetes resources which when created their ID is generated, so cannot be determined + * based only on primary resources. In order to manage such dependent resource use this interface + * for a resource that extends {@link AbstractExternalDependentResource}. + */ +public interface DependentResourceWithExplicitState + extends Creator, Deleter

, KubernetesClientAware { + + /** + * Only needs to be implemented if multiple event sources are present for the target resource + * class. + * + * @return name of the event source to access the state resources. + */ + default Optional eventSourceName() { + return Optional.empty(); + } + + /** + * Class of the state resource. + */ + Class stateResourceClass(); + + /** State resource which contains the target state. Usually an ID to address the resource */ + S stateResource(P primary, R resource); + +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/SingleDependentResourceReconciler.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/SingleDependentResourceReconciler.java index 20b37b3c7e..2862170c22 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/SingleDependentResourceReconciler.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/SingleDependentResourceReconciler.java @@ -21,6 +21,7 @@ public ReconcileResult reconcile(P primary, Context

context) { @Override public void delete(P primary, Context

context) { - instance.handleDelete(primary, context); + var secondary = instance.getSecondaryResource(primary, context); + instance.handleDelete(primary, secondary.orElse(null), context); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java index 2ccba025a7..6355ec39c7 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/external/AbstractPollingDependentResource.java @@ -2,14 +2,13 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.Ignore; -import io.javaoperatorsdk.operator.processing.dependent.AbstractEventSourceHolderDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.AbstractExternalDependentResource; import io.javaoperatorsdk.operator.processing.event.source.CacheKeyMapper; import io.javaoperatorsdk.operator.processing.event.source.ExternalResourceCachingEventSource; @Ignore public abstract class AbstractPollingDependentResource - extends - AbstractEventSourceHolderDependentResource> + extends AbstractExternalDependentResource> implements CacheKeyMapper { public static final int DEFAULT_POLLING_PERIOD = 5000; diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java index 070c8f8ffe..e16b2dd924 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/KubernetesDependentResource.java @@ -144,9 +144,10 @@ public Result match(R actualResource, R desired, P primary, Context

contex return GenericKubernetesResourceMatcher.match(desired, actualResource, false); } - protected void handleDelete(P primary, Context

context) { - var resource = getSecondaryResource(primary, context); - resource.ifPresent(r -> client.resource(r).delete()); + protected void handleDelete(P primary, R secondary, Context

context) { + if (secondary != null) { + client.resource(secondary).delete(); + } } @SuppressWarnings("unused") diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java index b93abd45c0..947596a710 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/AbstractDependentResourceTest.java @@ -7,7 +7,6 @@ import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; import io.javaoperatorsdk.operator.api.reconciler.Context; -import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.junit.jupiter.api.Assertions.*; @@ -85,10 +84,12 @@ public Optional getSecondaryResource(TestCustomResource primary, } @Override - protected void onCreated(ResourceID primaryResourceId, ConfigMap created) {} + protected void onCreated(TestCustomResource primary, ConfigMap created, + Context context) {} @Override - protected void onUpdated(ResourceID primaryResourceId, ConfigMap updated, ConfigMap actual) {} + protected void onUpdated(TestCustomResource primary, ConfigMap updated, ConfigMap actual, + Context context) {} @Override protected ConfigMap desired(TestCustomResource primary, Context context) { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java index a85543030b..7432e5da0a 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/EventSourceIT.java @@ -16,6 +16,7 @@ import static org.awaitility.Awaitility.await; class EventSourceIT { + @RegisterExtension LocallyRunOperatorExtension operator = LocallyRunOperatorExtension.builder().withReconciler(EventSourceTestCustomReconciler.class) diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateBulkIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateBulkIT.java new file mode 100644 index 0000000000..a52e9dc3c1 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateBulkIT.java @@ -0,0 +1,96 @@ +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.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.externalstate.externalstatebulkdependent.ExternalStateBulkDependentCustomResource; +import io.javaoperatorsdk.operator.sample.externalstate.externalstatebulkdependent.ExternalStateBulkDependentReconciler; +import io.javaoperatorsdk.operator.sample.externalstate.externalstatebulkdependent.ExternalStateBulkSpec; +import io.javaoperatorsdk.operator.support.ExternalIDGenServiceMock; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class ExternalStateBulkIT { + + private static final String TEST_RESOURCE_NAME = "test1"; + + public static final String INITIAL_TEST_DATA = "initialTestData"; + public static final String UPDATED_DATA = "updatedData"; + public static final int INITIAL_BULK_SIZE = 3; + public static final int INCREASED_BULK_SIZE = 4; + public static final int DECREASED_BULK_SIZE = 2; + + private ExternalIDGenServiceMock externalService = ExternalIDGenServiceMock.getInstance(); + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder() + .withReconciler(ExternalStateBulkDependentReconciler.class) + .build(); + + @Test + void reconcilesResourceWithPersistentState() throws InterruptedException { + var resource = operator.create(testResource()); + assertResources(resource, INITIAL_TEST_DATA, INITIAL_BULK_SIZE); + + resource.getSpec().setData(UPDATED_DATA); + resource = operator.replace(resource); + assertResources(resource, UPDATED_DATA, INITIAL_BULK_SIZE); + + resource.getSpec().setNumber(INCREASED_BULK_SIZE); + resource = operator.replace(resource); + assertResources(resource, UPDATED_DATA, INCREASED_BULK_SIZE); + + resource.getSpec().setNumber(DECREASED_BULK_SIZE); + resource = operator.replace(resource); + assertResources(resource, UPDATED_DATA, DECREASED_BULK_SIZE); + + operator.delete(resource); + assertResourcesDeleted(resource); + } + + private void assertResourcesDeleted(ExternalStateBulkDependentCustomResource resource) { + await().untilAsserted(() -> { + var configMaps = + operator.getKubernetesClient().configMaps().inNamespace(operator.getNamespace()) + .list().getItems().stream().filter( + cm -> cm.getMetadata().getName().startsWith(resource.getMetadata().getName())); + var resources = externalService.listResources(); + assertThat(configMaps).isEmpty(); + assertThat(resources).isEmpty(); + }); + } + + private void assertResources(ExternalStateBulkDependentCustomResource resource, + String initialTestData, int size) { + await().pollInterval(Duration.ofMillis(700)).untilAsserted(() -> { + var resources = externalService.listResources(); + assertThat(resources).hasSize(size); + assertThat(resources).allMatch(r -> r.getData().startsWith(initialTestData)); + + var configMaps = + operator.getKubernetesClient().configMaps().inNamespace(operator.getNamespace()) + .list().getItems().stream().filter( + cm -> cm.getMetadata().getName().startsWith(resource.getMetadata().getName())); + assertThat(configMaps).hasSize(size); + }); + } + + private ExternalStateBulkDependentCustomResource testResource() { + var res = new ExternalStateBulkDependentCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .build()); + + res.setSpec(new ExternalStateBulkSpec()); + res.getSpec().setNumber(INITIAL_BULK_SIZE); + res.getSpec().setData(INITIAL_TEST_DATA); + return res; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateDependentIT.java new file mode 100644 index 0000000000..fd7132bd2a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateDependentIT.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.externalstate.ExternalStateDependentReconciler; + +class ExternalStateDependentIT extends ExternalStateTestBase { + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder().withReconciler(ExternalStateDependentReconciler.class) + .build(); + + @Override + LocallyRunOperatorExtension extension() { + return operator; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateTestBase.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateTestBase.java new file mode 100644 index 0000000000..972b7e62dc --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ExternalStateTestBase.java @@ -0,0 +1,73 @@ +package io.javaoperatorsdk.operator; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.externalstate.ExternalStateCustomResource; +import io.javaoperatorsdk.operator.sample.externalstate.ExternalStateSpec; +import io.javaoperatorsdk.operator.support.ExternalIDGenServiceMock; + +import static io.javaoperatorsdk.operator.sample.externalstate.ExternalStateReconciler.ID_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public abstract class ExternalStateTestBase { + + private static final String TEST_RESOURCE_NAME = "test1"; + + public static final String INITIAL_TEST_DATA = "initialTestData"; + public static final String UPDATED_DATA = "updatedData"; + + private ExternalIDGenServiceMock externalService = ExternalIDGenServiceMock.getInstance(); + + @Test + public void reconcilesResourceWithPersistentState() { + var resource = extension().create(testResource()); + assertResourcesCreated(resource, INITIAL_TEST_DATA); + + resource.getSpec().setData(UPDATED_DATA); + extension().replace(resource); + assertResourcesCreated(resource, UPDATED_DATA); + + extension().delete(resource); + assertResourcesDeleted(resource); + } + + private void assertResourcesDeleted(ExternalStateCustomResource resource) { + await().untilAsserted(() -> { + var cm = extension().get(ConfigMap.class, resource.getMetadata().getName()); + var resources = externalService.listResources(); + assertThat(cm).isNull(); + assertThat(resources).isEmpty(); + }); + } + + private void assertResourcesCreated(ExternalStateCustomResource resource, + String initialTestData) { + await().untilAsserted(() -> { + var cm = extension().get(ConfigMap.class, resource.getMetadata().getName()); + var resources = externalService.listResources(); + assertThat(resources).hasSize(1); + var extRes = externalService.listResources().get(0); + assertThat(extRes.getData()).isEqualTo(initialTestData); + assertThat(cm).isNotNull(); + assertThat(cm.getData().get(ID_KEY)).isEqualTo(extRes.getId()); + }); + } + + private ExternalStateCustomResource testResource() { + var res = new ExternalStateCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .build()); + + res.setSpec(new ExternalStateSpec()); + res.getSpec().setData(INITIAL_TEST_DATA); + return res; + } + + abstract LocallyRunOperatorExtension extension(); + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/ExternalStateDependentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/ExternalStateDependentReconciler.java new file mode 100644 index 0000000000..8755e7099c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/ExternalStateDependentReconciler.java @@ -0,0 +1,45 @@ +package io.javaoperatorsdk.operator.sample.externalstate; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration( + dependents = @Dependent(type = ExternalWithStateDependentResource.class)) +public class ExternalStateDependentReconciler + implements Reconciler, + EventSourceInitializer, + TestExecutionInfoProvider { + + public static final String ID_KEY = "id"; + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + ExternalStateCustomResource resource, + Context context) { + numberOfExecutions.addAndGet(1); + + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + @Override + public Map prepareEventSources( + EventSourceContext context) { + var configMapEventSource = new InformerEventSource<>( + InformerConfiguration.from(ConfigMap.class, context).build(), context); + return EventSourceInitializer.nameEventSources(configMapEventSource); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/ExternalWithStateDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/ExternalWithStateDependentResource.java new file mode 100644 index 0000000000..b1236b7126 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/ExternalWithStateDependentResource.java @@ -0,0 +1,99 @@ +package io.javaoperatorsdk.operator.sample.externalstate; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.DependentResourceWithExplicitState; +import io.javaoperatorsdk.operator.processing.dependent.Matcher; +import io.javaoperatorsdk.operator.processing.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; +import io.javaoperatorsdk.operator.support.ExternalIDGenServiceMock; +import io.javaoperatorsdk.operator.support.ExternalResource; + +import static io.javaoperatorsdk.operator.sample.externalstate.ExternalStateDependentReconciler.ID_KEY; + +public class ExternalWithStateDependentResource extends + PerResourcePollingDependentResource + implements + DependentResourceWithExplicitState, + Updater { + + ExternalIDGenServiceMock externalService = ExternalIDGenServiceMock.getInstance(); + + public ExternalWithStateDependentResource() { + super(ExternalResource.class, 300); + } + + @Override + @SuppressWarnings("unchecked") + public Set fetchResources( + ExternalStateCustomResource primaryResource) { + Optional configMapOptional = + getExternalStateEventSource().getSecondaryResource(primaryResource); + + return configMapOptional.map(configMap -> { + var id = configMap.getData().get(ID_KEY); + var externalResource = externalService.read(id); + return externalResource.map(er -> Set.of(er)).orElse(Collections.emptySet()); + }).orElse(Collections.emptySet()); + } + + @Override + protected ExternalResource desired(ExternalStateCustomResource primary, + Context context) { + return new ExternalResource(primary.getSpec().getData()); + } + + @Override + public Class stateResourceClass() { + return ConfigMap.class; + } + + @Override + public ConfigMap stateResource(ExternalStateCustomResource primary, + ExternalResource resource) { + ConfigMap configMap = new ConfigMapBuilder() + .withMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()) + .withData(Map.of(ID_KEY, resource.getId())) + .build(); + configMap.addOwnerReference(primary); + return configMap; + } + + @Override + public ExternalResource create(ExternalResource desired, + ExternalStateCustomResource primary, + Context context) { + return externalService.create(desired); + } + + @Override + public ExternalResource update(ExternalResource actual, + ExternalResource desired, ExternalStateCustomResource primary, + Context context) { + return externalService.update(new ExternalResource(actual.getId(), desired.getData())); + } + + @Override + public Matcher.Result match(ExternalResource resource, + ExternalStateCustomResource primary, + Context context) { + return Matcher.Result.nonComputed(resource.getData().equals(primary.getSpec().getData())); + } + + @Override + protected void handleDelete(ExternalStateCustomResource primary, + ExternalResource secondary, + Context context) { + externalService.delete(secondary.getId()); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/BulkDependentResourceExternalWithState.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/BulkDependentResourceExternalWithState.java new file mode 100644 index 0000000000..5344810609 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/BulkDependentResourceExternalWithState.java @@ -0,0 +1,137 @@ +package io.javaoperatorsdk.operator.sample.externalstate.externalstatebulkdependent; + +import java.util.*; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.*; +import io.javaoperatorsdk.operator.processing.dependent.external.PerResourcePollingDependentResource; +import io.javaoperatorsdk.operator.support.ExternalIDGenServiceMock; +import io.javaoperatorsdk.operator.support.ExternalResource; + +import static io.javaoperatorsdk.operator.sample.externalstate.ExternalStateDependentReconciler.ID_KEY; + +public class BulkDependentResourceExternalWithState extends + PerResourcePollingDependentResource + implements + BulkDependentResource, + DependentResourceWithExplicitState, + BulkUpdater { + + public static final String DELIMITER = "-"; + ExternalIDGenServiceMock externalService = ExternalIDGenServiceMock.getInstance(); + + public BulkDependentResourceExternalWithState() { + super(ExternalResource.class, 300); + } + + @Override + @SuppressWarnings("unchecked") + public Set fetchResources( + ExternalStateBulkDependentCustomResource primaryResource) { + Set configMaps = + getExternalStateEventSource().getSecondaryResources(primaryResource); + Set res = new HashSet<>(); + + configMaps.stream().forEach(cm -> { + var id = cm.getData().get(ID_KEY); + var externalResource = externalService.read(id); + externalResource.ifPresent(er -> res.add(er)); + }); + return res; + } + + @Override + public Class stateResourceClass() { + return ConfigMap.class; + } + + @Override + public ConfigMap stateResource(ExternalStateBulkDependentCustomResource primary, + ExternalResource resource) { + ConfigMap configMap = new ConfigMapBuilder() + .withMetadata(new ObjectMetaBuilder() + .withName(configMapName(primary, resource)) + .withNamespace(primary.getMetadata().getNamespace()) + .build()) + .withData(Map.of(ID_KEY, resource.getId())) + .build(); + configMap.addOwnerReference(primary); + return configMap; + } + + @Override + public ExternalResource create(ExternalResource desired, + ExternalStateBulkDependentCustomResource primary, + Context context) { + return externalService.create(desired); + } + + @Override + public ExternalResource update(ExternalResource actual, + ExternalResource desired, ExternalStateBulkDependentCustomResource primary, + Context context) { + return externalService.update(new ExternalResource(actual.getId(), desired.getData())); + } + + @Override + protected void handleDelete(ExternalStateBulkDependentCustomResource primary, + ExternalResource secondary, + Context context) { + externalService.delete(secondary.getId()); + } + + @Override + public Matcher.Result match(ExternalResource actualResource, + ExternalResource desired, + ExternalStateBulkDependentCustomResource primary, + Context context) { + return Matcher.Result.computed(desired.getData().equals(actualResource.getData()), desired); + } + + @Override + public Map desiredResources( + ExternalStateBulkDependentCustomResource primary, + Context context) { + int number = primary.getSpec().getNumber(); + Map res = new HashMap<>(); + for (int i = 0; i < number; i++) { + res.put(Integer.toString(i), + new ExternalResource(primary.getSpec().getData() + DELIMITER + i)); + } + return res; + } + + @Override + public Map getSecondaryResources( + ExternalStateBulkDependentCustomResource primary, + Context context) { + var resources = context.getSecondaryResources(ExternalResource.class); + return resources.stream().collect(Collectors.toMap(this::externalResourceIndex, r -> r)); + } + + @Override + public void handleDeleteTargetResource(ExternalStateBulkDependentCustomResource primary, + ExternalResource resource, String key, + Context context) { + externalService.delete(resource.getId()); + } + + private String externalResourceIndex(ExternalResource externalResource) { + return externalResource.getData() + .substring(externalResource.getData().lastIndexOf(DELIMITER) + 1); + } + + private String configMapName(ExternalStateBulkDependentCustomResource primary, + ExternalResource resource) { + return primary.getMetadata().getName() + DELIMITER + externalResourceIndex(resource); + } + + @Override + public String keyFor(ExternalResource resource) { + return externalResourceIndex(resource); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentCustomResource.java new file mode 100644 index 0000000000..9ee3e260e8 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentCustomResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.externalstate.externalstatebulkdependent; + +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("esb") +public class ExternalStateBulkDependentCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentReconciler.java new file mode 100644 index 0000000000..a572572a97 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkDependentReconciler.java @@ -0,0 +1,46 @@ +package io.javaoperatorsdk.operator.sample.externalstate.externalstatebulkdependent; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +@ControllerConfiguration( + dependents = @Dependent( + type = BulkDependentResourceExternalWithState.class)) +public class ExternalStateBulkDependentReconciler + implements Reconciler, + EventSourceInitializer, + TestExecutionInfoProvider { + + public static final String ID_KEY = "id"; + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + ExternalStateBulkDependentCustomResource resource, + Context context) { + numberOfExecutions.addAndGet(1); + + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + @Override + public Map prepareEventSources( + EventSourceContext context) { + var configMapEventSource = new InformerEventSource<>( + InformerConfiguration.from(ConfigMap.class, context).build(), context); + return EventSourceInitializer.nameEventSources(configMapEventSource); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkSpec.java new file mode 100644 index 0000000000..ad52a389fd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/externalstate/externalstatebulkdependent/ExternalStateBulkSpec.java @@ -0,0 +1,25 @@ +package io.javaoperatorsdk.operator.sample.externalstate.externalstatebulkdependent; + +public class ExternalStateBulkSpec { + + private Integer number; + private String data; + + public String getData() { + return data; + } + + public ExternalStateBulkSpec setData(String data) { + this.data = data; + return this; + } + + public Integer getNumber() { + return number; + } + + public ExternalStateBulkSpec setNumber(Integer number) { + this.number = number; + return this; + } +}