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 05c5b26a0e..5b923b8dca 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 @@ -60,7 +60,7 @@ public ReconcileResult reconcile(P primary, Context

context) { } protected ReconcileResult reconcile(P primary, R actualResource, Context

context) { - if (creatable || updatable) { + if (creatable() || updatable()) { if (actualResource == null) { if (creatable) { var desired = desired(primary, context); @@ -70,7 +70,7 @@ protected ReconcileResult reconcile(P primary, R actualResource, Context

c return ReconcileResult.resourceCreated(createdResource); } } else { - if (updatable) { + if (updatable()) { final Matcher.Result match = match(actualResource, primary, context); if (!match.matched()) { final var desired = match.computedDesired().orElseGet(() -> desired(primary, context)); @@ -216,4 +216,12 @@ public String name() { public void setName(String name) { this.name = name; } + + protected boolean creatable() { + return creatable; + } + + protected boolean updatable() { + return updatable; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResource.java index a86b6ee0e8..313d7115c9 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/BulkDependentResource.java @@ -13,8 +13,7 @@ * {@link Creator} and {@link Deleter} interfaces out of the box. A concrete dependent resource can * implement additionally also {@link Updater}. */ -public interface BulkDependentResource - extends Creator, Deleter

{ +public interface BulkDependentResource { /** * Retrieves a map of desired secondary resources associated with the specified primary resource, @@ -26,7 +25,10 @@ public interface BulkDependentResource * @return a Map associating desired secondary resources with the specified primary via arbitrary * identifiers */ - Map desiredResources(P primary, Context

context); + default Map desiredResources(P primary, Context

context) { + throw new IllegalStateException( + "Implement desiredResources in case a non read-only bulk dependent resource"); + } /** * Retrieves the actual secondary resources currently existing on the server and associated with 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 e740ef3773..1ed36edaa2 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 @@ -24,18 +24,26 @@ class BulkDependentResourceReconciler @Override public ReconcileResult reconcile(P primary, Context

context) { - final var desiredResources = bulkDependentResource.desiredResources(primary, context); + Map actualResources = bulkDependentResource.getSecondaryResources(primary, context); + if (!(bulkDependentResource instanceof Creator) + && !(bulkDependentResource instanceof Deleter) + && !(bulkDependentResource instanceof Updater)) { + return ReconcileResult + .aggregatedResult(actualResources.values().stream().map(ReconcileResult::noOperation) + .toList()); + } - // remove existing resources that are not needed anymore according to the primary state - deleteExtraResources(desiredResources.keySet(), actualResources, primary, context); + final var desiredResources = bulkDependentResource.desiredResources(primary, context); + + if (bulkDependentResource instanceof Deleter) { + // remove existing resources that are not needed anymore according to the primary state + deleteExtraResources(desiredResources.keySet(), actualResources, primary, context); + } final List> results = new ArrayList<>(desiredResources.size()); - final var updatable = bulkDependentResource instanceof Updater; desiredResources.forEach((key, value) -> { - final var instance = - updatable ? new UpdatableBulkDependentResourceInstance<>(bulkDependentResource, value) - : new BulkDependentResourceInstance<>(bulkDependentResource, value); + final var instance = new BulkDependentResourceInstance<>(bulkDependentResource, value); results.add(instance.reconcile(primary, actualResources.get(key), context)); }); @@ -67,7 +75,7 @@ private void deleteExtraResources(Set expectedKeys, @Ignore private static class BulkDependentResourceInstance extends AbstractDependentResource - implements Creator, Deleter

{ + implements Creator, Deleter

, Updater { private final BulkDependentResource bulkDependentResource; private final R desired; @@ -112,26 +120,24 @@ public Class resourceType() { return asAbstractDependentResource().resourceType(); } - @Override + @SuppressWarnings("unchecked") public R create(R desired, P primary, Context

context) { - return bulkDependentResource.create(desired, primary, context); + return ((Creator) bulkDependentResource).create(desired, primary, context); } - } - /** - * Makes sure that the instance implements Updater if its precursor does as well. - * - * @param - * @param

- */ - @Ignore - private static class UpdatableBulkDependentResourceInstance - extends BulkDependentResourceInstance implements Updater { + @Override + protected boolean isCreatable() { + return bulkDependentResource instanceof Creator; + } - private UpdatableBulkDependentResourceInstance( - BulkDependentResource bulkDependentResource, - R desired) { - super(bulkDependentResource, desired); + @Override + protected boolean isUpdatable() { + return bulkDependentResource instanceof Updater; + } + + @Override + public boolean isDeletable() { + return bulkDependentResource instanceof Deleter; } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/CRUDBulkDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/CRUDBulkDependentResource.java new file mode 100644 index 0000000000..aa650bf1b5 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/CRUDBulkDependentResource.java @@ -0,0 +1,11 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; + +public interface CRUDBulkDependentResource + extends BulkDependentResource, + Creator, + BulkUpdater, + Deleter

{ +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/ReadOnlyBulkDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ReadOnlyBulkDependentIT.java new file mode 100644 index 0000000000..265eb98e4e --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/ReadOnlyBulkDependentIT.java @@ -0,0 +1,73 @@ +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.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource; +import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestSpec; +import io.javaoperatorsdk.operator.sample.bulkdependent.readonly.ReadOnlyBulkDependentResource; +import io.javaoperatorsdk.operator.sample.bulkdependent.readonly.ReadOnlyBulkReconciler; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class ReadOnlyBulkDependentIT { + + public static final int EXPECTED_NUMBER_OF_RESOURCES = 2; + public static final String TEST = "test"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(new ReadOnlyBulkReconciler()) + .build(); + + @Test + void readOnlyBulkDependent() { + var primary = extension.create(testCustomResource()); + + await().pollDelay(Duration.ofMillis(150)).untilAsserted(() -> { + var actualPrimary = extension.get(BulkDependentTestCustomResource.class, TEST); + + assertThat(actualPrimary.getStatus()).isNotNull(); + assertThat(actualPrimary.getStatus().getReady()).isFalse(); + }); + + var configMap1 = createConfigMap(1, primary); + extension.create(configMap1); + var configMap2 = createConfigMap(2, primary); + extension.create(configMap2); + + await().untilAsserted(() -> { + var actualPrimary = extension.get(BulkDependentTestCustomResource.class, TEST); + assertThat(actualPrimary.getStatus().getReady()).isTrue(); + }); + } + + private ConfigMap createConfigMap(int i, BulkDependentTestCustomResource primary) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMetaBuilder() + .withName(TEST + ReadOnlyBulkDependentResource.INDEX_DELIMITER + i) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + configMap.addOwnerReference(primary); + return configMap; + } + + BulkDependentTestCustomResource testCustomResource() { + BulkDependentTestCustomResource customResource = new BulkDependentTestCustomResource(); + customResource.setMetadata(new ObjectMetaBuilder() + .withName(TEST) + .build()); + customResource.setSpec(new BulkDependentTestSpec()); + customResource.getSpec().setNumberOfResources(EXPECTED_NUMBER_OF_RESOURCES); + + return customResource; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ConfigMapDeleterBulkDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ConfigMapDeleterBulkDependentResource.java index cb52ebdd05..1ee4e3ef24 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ConfigMapDeleterBulkDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ConfigMapDeleterBulkDependentResource.java @@ -8,10 +8,7 @@ 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.api.reconciler.dependent.Deleter; -import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.Creator; -import io.javaoperatorsdk.operator.processing.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.*; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; /** @@ -20,10 +17,7 @@ public class ConfigMapDeleterBulkDependentResource extends KubernetesDependentResource - implements Creator, - Updater, - Deleter, - BulkDependentResource { + implements CRUDBulkDependentResource { public static final String LABEL_KEY = "bulk"; public static final String LABEL_VALUE = "true"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalBulkDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalBulkDependentResource.java index dbad6dcfb8..7dee7ae962 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalBulkDependentResource.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/external/ExternalBulkDependentResource.java @@ -7,8 +7,10 @@ import java.util.stream.Collectors; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource; import io.javaoperatorsdk.operator.processing.dependent.BulkUpdater; +import io.javaoperatorsdk.operator.processing.dependent.Creator; import io.javaoperatorsdk.operator.processing.dependent.external.PollingDependentResource; import io.javaoperatorsdk.operator.processing.event.ResourceID; import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource; @@ -16,6 +18,8 @@ public class ExternalBulkDependentResource extends PollingDependentResource implements BulkDependentResource, + Creator, + Deleter, BulkUpdater { public static final String EXTERNAL_RESOURCE_NAME_DELIMITER = "#"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/readonly/ReadOnlyBulkDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/readonly/ReadOnlyBulkDependentResource.java new file mode 100644 index 0000000000..b812b82ef1 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/readonly/ReadOnlyBulkDependentResource.java @@ -0,0 +1,53 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent.readonly; + +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper; +import io.javaoperatorsdk.operator.processing.event.source.informer.Mappers; +import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource; + + +public class ReadOnlyBulkDependentResource + extends + KubernetesDependentResource + implements BulkDependentResource, + SecondaryToPrimaryMapper { + + public static final String INDEX_DELIMITER = "-"; + + public ReadOnlyBulkDependentResource() { + super(ConfigMap.class); + } + + @Override + protected Class getPrimaryResourceType() { + return BulkDependentTestCustomResource.class; + } + + @Override + public Map getSecondaryResources(BulkDependentTestCustomResource primary, + Context context) { + return context.getSecondaryResourcesAsStream(ConfigMap.class) + .filter(cm -> getName(cm).startsWith(primary.getMetadata().getName())) + .collect(Collectors.toMap( + cm -> getName(cm).substring(getName(cm).lastIndexOf(INDEX_DELIMITER) + 1), + Function.identity())); + } + + private static String getName(ConfigMap cm) { + return cm.getMetadata().getName(); + } + + @Override + public Set toPrimaryResourceIDs(ConfigMap resource) { + return Mappers.fromOwnerReferences(false).toPrimaryResourceIDs(resource); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/readonly/ReadOnlyBulkReadyPostCondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/readonly/ReadOnlyBulkReadyPostCondition.java new file mode 100644 index 0000000000..9d036d74bd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/readonly/ReadOnlyBulkReadyPostCondition.java @@ -0,0 +1,23 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent.readonly; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.dependent.BulkDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; +import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource; + +public class ReadOnlyBulkReadyPostCondition + implements Condition { + @Override + public boolean isMet( + DependentResource dependentResource, + BulkDependentTestCustomResource primary, Context context) { + var minResourceNumber = primary.getSpec().getNumberOfResources(); + @SuppressWarnings("unchecked") + var secondaryResources = + ((BulkDependentResource) dependentResource) + .getSecondaryResources(primary, context); + return minResourceNumber <= secondaryResources.size(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/readonly/ReadOnlyBulkReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/readonly/ReadOnlyBulkReconciler.java new file mode 100644 index 0000000000..df3366c115 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/readonly/ReadOnlyBulkReconciler.java @@ -0,0 +1,33 @@ +package io.javaoperatorsdk.operator.sample.bulkdependent.readonly; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; +import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestCustomResource; +import io.javaoperatorsdk.operator.sample.bulkdependent.BulkDependentTestStatus; + +@Workflow(dependents = @Dependent(type = ReadOnlyBulkDependentResource.class, + readyPostcondition = ReadOnlyBulkReadyPostCondition.class)) +@ControllerConfiguration +public class ReadOnlyBulkReconciler implements Reconciler { + @Override + public UpdateControl reconcile( + BulkDependentTestCustomResource resource, Context context) { + + var nonReadyDependents = + context.managedWorkflowAndDependentResourceContext().getWorkflowReconcileResult() + .getNotReadyDependents(); + + + BulkDependentTestCustomResource customResource = new BulkDependentTestCustomResource(); + customResource.setMetadata(new ObjectMetaBuilder() + .withName(resource.getMetadata().getName()) + .withNamespace(resource.getMetadata().getNamespace()) + .build()); + var status = new BulkDependentTestStatus(); + status.setReady(nonReadyDependents.isEmpty()); + customResource.setStatus(status); + + return UpdateControl.patchStatus(customResource); + } +} 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 index 29bc6af9aa..48e207616d 100644 --- 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 @@ -11,10 +11,7 @@ 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.BulkDependentResource; -import io.javaoperatorsdk.operator.processing.dependent.BulkUpdater; -import io.javaoperatorsdk.operator.processing.dependent.DependentResourceWithExplicitState; -import io.javaoperatorsdk.operator.processing.dependent.Matcher; +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; @@ -25,8 +22,8 @@ public class BulkDependentResourceExternalWithState extends PerResourcePollingDependentResource implements BulkDependentResource, - DependentResourceWithExplicitState, - BulkUpdater { + CRUDBulkDependentResource, + DependentResourceWithExplicitState { public static final String DELIMITER = "-"; ExternalIDGenServiceMock externalService = ExternalIDGenServiceMock.getInstance();