diff --git a/docs/documentation/v4-3-migration.md b/docs/documentation/v4-3-migration.md index 77979fe3ba..f5022fa856 100644 --- a/docs/documentation/v4-3-migration.md +++ b/docs/documentation/v4-3-migration.md @@ -9,8 +9,8 @@ permalink: /docs/v4-3-migration ## Condition API Change -In Workflows the target of the condition was the managed resource itself, not a dependent resource. This changed, from -not the API contains the dependent resource. +In Workflows the target of the condition was the managed resource itself, not the target dependent resource. +This changed, now the API contains the dependent resource. New API: diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryToSecondaryDependentIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryToSecondaryDependentIT.java new file mode 100644 index 0000000000..eaa7e4410f --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/PrimaryToSecondaryDependentIT.java @@ -0,0 +1,76 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; +import java.util.Map; + +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.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentCustomResource; +import io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler; +import io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentSpec; + +import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.ConfigMapReconcilePrecondition.DO_NOT_RECONCILE; +import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.DATA_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class PrimaryToSecondaryDependentIT { + + public static final String TEST_CONFIG_MAP_NAME = "testconfigmap"; + public static final String TEST_CR_NAME = "test1"; + public static final String TEST_DATA = "testData"; + public + + @RegisterExtension LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder() + .withReconciler(new PrimaryToSecondaryDependentReconciler()) + .build(); + + @Test + void testPrimaryToSecondaryInDependentResources() { + var reconciler = operator.getReconcilerOfType(PrimaryToSecondaryDependentReconciler.class); + var cm = operator.create(configMap(DO_NOT_RECONCILE)); + operator.create(testCustomResource()); + + await().pollDelay(Duration.ofMillis(250)).untilAsserted(() -> { + assertThat(reconciler.getNumberOfExecutions()).isPositive(); + assertThat(operator.get(Secret.class, TEST_CR_NAME)).isNull(); + }); + + cm.setData(Map.of(DATA_KEY, TEST_DATA)); + var executions = reconciler.getNumberOfExecutions(); + operator.replace(cm); + + await().pollDelay(Duration.ofMillis(250)).untilAsserted(() -> { + assertThat(reconciler.getNumberOfExecutions()).isGreaterThan(executions); + var secret = operator.get(Secret.class, TEST_CR_NAME); + assertThat(secret).isNotNull(); + assertThat(secret.getData().get(DATA_KEY)).isEqualTo(TEST_DATA); + }); + } + + PrimaryToSecondaryDependentCustomResource testCustomResource() { + var res = new PrimaryToSecondaryDependentCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(TEST_CR_NAME) + .build()); + res.setSpec(new PrimaryToSecondaryDependentSpec()); + res.getSpec().setConfigMapName(TEST_CONFIG_MAP_NAME); + return res; + } + + ConfigMap configMap(String data) { + var cm = new ConfigMap(); + cm.setMetadata(new ObjectMetaBuilder() + .withName(TEST_CONFIG_MAP_NAME) + .build()); + cm.setData(Map.of(DATA_KEY, data)); + return cm; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/ConfigMapDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/ConfigMapDependent.java new file mode 100644 index 0000000000..d08bc2131f --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/ConfigMapDependent.java @@ -0,0 +1,12 @@ +package io.javaoperatorsdk.operator.sample.primarytosecondaydependent; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; + +public class ConfigMapDependent extends + KubernetesDependentResource { + + public ConfigMapDependent() { + super(ConfigMap.class); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/ConfigMapReconcilePrecondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/ConfigMapReconcilePrecondition.java new file mode 100644 index 0000000000..b8aa65585d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/ConfigMapReconcilePrecondition.java @@ -0,0 +1,26 @@ +package io.javaoperatorsdk.operator.sample.primarytosecondaydependent; + +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.workflow.Condition; + +import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.DATA_KEY; + +public class ConfigMapReconcilePrecondition + implements Condition { + + public static final String DO_NOT_RECONCILE = "doNotReconcile"; + + @Override + public boolean isMet( + DependentResource dependentResource, + PrimaryToSecondaryDependentCustomResource primary, + Context context) { + return dependentResource.getSecondaryResource(primary, context).map(cm -> { + var data = cm.getData().get(DATA_KEY); + return data != null && !data.equals(DO_NOT_RECONCILE); + }) + .orElse(false); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/PrimaryToSecondaryDependentCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/PrimaryToSecondaryDependentCustomResource.java new file mode 100644 index 0000000000..4ec7065f2e --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/PrimaryToSecondaryDependentCustomResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.primarytosecondaydependent; + +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("ptsd") +public class PrimaryToSecondaryDependentCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/PrimaryToSecondaryDependentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/PrimaryToSecondaryDependentReconciler.java new file mode 100644 index 0000000000..c51111b206 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/PrimaryToSecondaryDependentReconciler.java @@ -0,0 +1,92 @@ +package io.javaoperatorsdk.operator.sample.primarytosecondaydependent; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +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.ResourceID; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; +import io.javaoperatorsdk.operator.processing.event.source.PrimaryToSecondaryMapper; +import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource; +import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; + +import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.CONFIG_MAP; +import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.CONFIG_MAP_EVENT_SOURCE; + +/** + * Sample showcases how it is possible to do a primary to secondary mapper for a dependent resource. + * Note that this is usually just used with read only resources. So it has limited usage, one reason + * to use it is to have nice condition on that resource within a workflow. + */ +@ControllerConfiguration(dependents = {@Dependent(type = ConfigMapDependent.class, + name = CONFIG_MAP, + reconcilePrecondition = ConfigMapReconcilePrecondition.class, + useEventSourceWithName = CONFIG_MAP_EVENT_SOURCE), + @Dependent(type = SecretDependent.class, dependsOn = CONFIG_MAP)}) +public class PrimaryToSecondaryDependentReconciler + implements Reconciler, TestExecutionInfoProvider, + EventSourceInitializer { + + public static final String DATA_KEY = "data"; + public static final String CONFIG_MAP = "ConfigMap"; + public static final String CONFIG_MAP_INDEX = "ConfigMapIndex"; + public static final String CONFIG_MAP_EVENT_SOURCE = "ConfigMapEventSource"; + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + PrimaryToSecondaryDependentCustomResource resource, + Context context) { + numberOfExecutions.addAndGet(1); + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + /** + * Creating an Event Source and setting it for the Dependent Resource. Since it is not possible to + * do this setup elegantly within the bounds of the KubernetesDependentResource API. However, this + * is quite a corner case; might be covered more out of the box in the future if there will be + * demand for it. + **/ + @Override + public Map prepareEventSources( + EventSourceContext context) { + // there is no owner reference in the config map, but we still want to trigger reconciliation if + // the config map changes. So first we add an index which custom resource references the config + // map. + context.getPrimaryCache().addIndexer(CONFIG_MAP_INDEX, (primary -> List + .of(indexKey(primary.getSpec().getConfigMapName(), primary.getMetadata().getNamespace())))); + + var cmES = new InformerEventSource<>(InformerConfiguration + .from(ConfigMap.class, context) + // if there is a many-to-many relationship (thus no direct owner reference) + // PrimaryToSecondaryMapper needs to be added + .withPrimaryToSecondaryMapper( + (PrimaryToSecondaryMapper) p -> Set + .of(new ResourceID(p.getSpec().getConfigMapName(), p.getMetadata().getNamespace()))) + // the index is used to trigger reconciliation of related custom resources if config map + // changes + .withSecondaryToPrimaryMapper(cm -> context.getPrimaryCache() + .byIndex(CONFIG_MAP_INDEX, indexKey(cm.getMetadata().getName(), + cm.getMetadata().getNamespace())) + .stream().map(ResourceID::fromResource).collect(Collectors.toSet())) + .build(), + context); + + return Map.of(CONFIG_MAP_EVENT_SOURCE, cmES); + } + + private String indexKey(String configMapName, String namespace) { + return configMapName + "#" + namespace; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/PrimaryToSecondaryDependentSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/PrimaryToSecondaryDependentSpec.java new file mode 100644 index 0000000000..966a5c242b --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/PrimaryToSecondaryDependentSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.primarytosecondaydependent; + +public class PrimaryToSecondaryDependentSpec { + + private String configMapName; + + public String getConfigMapName() { + return configMapName; + } + + public PrimaryToSecondaryDependentSpec setConfigMapName(String configMapName) { + this.configMapName = configMapName; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/SecretDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/SecretDependent.java new file mode 100644 index 0000000000..c78f3c470f --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/primarytosecondaydependent/SecretDependent.java @@ -0,0 +1,32 @@ +package io.javaoperatorsdk.operator.sample.primarytosecondaydependent; + +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; + +import static io.javaoperatorsdk.operator.sample.primarytosecondaydependent.PrimaryToSecondaryDependentReconciler.DATA_KEY; + +public class SecretDependent + extends CRUDKubernetesDependentResource { + + public SecretDependent() { + super(Secret.class); + } + + @Override + protected Secret desired(PrimaryToSecondaryDependentCustomResource primary, + Context context) { + Secret secret = new Secret(); + secret.setMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + secret.setData(Map.of(DATA_KEY, context.getSecondaryResource(ConfigMap.class) + .orElseThrow().getData().get(DATA_KEY))); + return secret; + } +}