diff --git a/docs/content/en/docs/workflows/_index.md b/docs/content/en/docs/workflows/_index.md index 83272ec3a9..cbb39840b0 100644 --- a/docs/content/en/docs/workflows/_index.md +++ b/docs/content/en/docs/workflows/_index.md @@ -48,9 +48,11 @@ reconciliation process. with the dependent's resource type is not present on the cluster. See related [integration test](https://github.com/operator-framework/java-operator-sdk/blob/ba5e33527bf9e3ea0bd33025ccb35e677f9d44b4/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDPresentActivationConditionIT.java). - Activation condition is semi-experimental at the moment, and it has its limitations. - For example event sources cannot be shared between multiple managed dependent resources which use activation condition. - The intention is to further improve and explore the possibilities with this approach. + To have multiple resources of same type with an activation condition is a bit tricky, since you + don't want to have multiple `InformerEvetnSource` for the same type, you have to explicitly + name the informer for the Dependent Resource (`@KubernetesDependent(informerConfig = @InformerConfig(name = "configMapInformer"))`) + for all resource of same type with activation condition. This will make sure that only one is registered. + See details at [low level api](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java#L20-L52). ## Defining Workflows diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleDependentWithActivationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleDependentWithActivationIT.java new file mode 100644 index 0000000000..e5d1542b19 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/MultipleDependentWithActivationIT.java @@ -0,0 +1,76 @@ +package io.javaoperatorsdk.operator; + +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.multipledependentwithactivation.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class MultipleDependentWithActivationIT { + + public static final String INITIAL_VALUE = "initial_value"; + public static final String CHANGED_VALUE = "changed_value"; + public static final String TEST_RESOURCE_NAME = "test1"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(new MultipleDependentActivationReconciler()) + .build(); + + @Test + void bothDependentsWithActivationAreHandled() { + var resource = extension.create(testResource()); + + await().untilAsserted(() -> { + var cm1 = + extension.get(ConfigMap.class, TEST_RESOURCE_NAME + ConfigMapDependentResource1.SUFFIX); + var cm2 = + extension.get(ConfigMap.class, TEST_RESOURCE_NAME + ConfigMapDependentResource2.SUFFIX); + var secret = extension.get(Secret.class, TEST_RESOURCE_NAME); + assertThat(secret).isNotNull(); + assertThat(cm1).isNull(); + assertThat(cm2).isNull(); + }); + + ActivationCondition.MET = true; + resource.getSpec().setValue(CHANGED_VALUE); + extension.replace(resource); + + await().untilAsserted(() -> { + var cm1 = + extension.get(ConfigMap.class, TEST_RESOURCE_NAME + ConfigMapDependentResource1.SUFFIX); + var cm2 = + extension.get(ConfigMap.class, TEST_RESOURCE_NAME + ConfigMapDependentResource2.SUFFIX); + var secret = extension.get(Secret.class, TEST_RESOURCE_NAME); + + assertThat(secret).isNotNull(); + assertThat(cm1).isNotNull(); + assertThat(cm2).isNotNull(); + assertThat(cm1.getData()).containsEntry(ConfigMapDependentResource1.DATA_KEY, + CHANGED_VALUE + ConfigMapDependentResource1.SUFFIX); + assertThat(cm2.getData()).containsEntry(ConfigMapDependentResource2.DATA_KEY, + CHANGED_VALUE + ConfigMapDependentResource2.SUFFIX); + }); + + } + + MultipleDependentActivationCustomResource testResource() { + var res = new MultipleDependentActivationCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .build()); + res.setSpec(new MultipleDependentActivationSpec()); + res.getSpec().setValue(INITIAL_VALUE); + + return res; + } + + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/ActivationCondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/ActivationCondition.java new file mode 100644 index 0000000000..5e357351f5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/ActivationCondition.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.sample.multipledependentwithactivation; + +import io.fabric8.openshift.api.model.Route; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; + +public class ActivationCondition + implements Condition { + + public static volatile boolean MET = false; + + @Override + public boolean isMet( + DependentResource dependentResource, + MultipleDependentActivationCustomResource primary, + Context context) { + return MET; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/ConfigMapDependentResource1.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/ConfigMapDependentResource1.java new file mode 100644 index 0000000000..e9b53898b8 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/ConfigMapDependentResource1.java @@ -0,0 +1,35 @@ +package io.javaoperatorsdk.operator.sample.multipledependentwithactivation; + +import java.util.Map; + +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.dependent.kubernetes.CRUDNoGCKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.InformerConfig; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; + +@KubernetesDependent(informerConfig = @InformerConfig(name = "configMapInformer")) +public class ConfigMapDependentResource1 + extends + CRUDNoGCKubernetesDependentResource { + + public static final String DATA_KEY = "data"; + public static final String SUFFIX = "1"; + + public ConfigMapDependentResource1() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(MultipleDependentActivationCustomResource primary, + Context context) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName() + SUFFIX) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + configMap.setData(Map.of(DATA_KEY, primary.getSpec().getValue() + SUFFIX)); + return configMap; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/ConfigMapDependentResource2.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/ConfigMapDependentResource2.java new file mode 100644 index 0000000000..c88929a61f --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/ConfigMapDependentResource2.java @@ -0,0 +1,35 @@ +package io.javaoperatorsdk.operator.sample.multipledependentwithactivation; + +import java.util.Map; + +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.dependent.kubernetes.CRUDNoGCKubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.InformerConfig; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; + +@KubernetesDependent(informerConfig = @InformerConfig(name = "configMapInformer")) +public class ConfigMapDependentResource2 + extends + CRUDNoGCKubernetesDependentResource { + + public static final String DATA_KEY = "data"; + public static final String SUFFIX = "2"; + + public ConfigMapDependentResource2() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(MultipleDependentActivationCustomResource primary, + Context context) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName() + SUFFIX) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + configMap.setData(Map.of(DATA_KEY, primary.getSpec().getValue() + SUFFIX)); + return configMap; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/MultipleDependentActivationCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/MultipleDependentActivationCustomResource.java new file mode 100644 index 0000000000..e373a7bc01 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/MultipleDependentActivationCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.multipledependentwithactivation; + +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("mdar") +public class MultipleDependentActivationCustomResource + extends CustomResource + implements Namespaced { + + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/MultipleDependentActivationReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/MultipleDependentActivationReconciler.java new file mode 100644 index 0000000000..5a4961c6c6 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/MultipleDependentActivationReconciler.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.sample.multipledependentwithactivation; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +@Workflow(dependents = { + @Dependent(type = ConfigMapDependentResource1.class, + activationCondition = ActivationCondition.class), + @Dependent(type = ConfigMapDependentResource2.class, + activationCondition = ActivationCondition.class), + @Dependent(type = SecretDependentResource.class) +}) +@ControllerConfiguration +public class MultipleDependentActivationReconciler + implements Reconciler { + + private final AtomicInteger numberOfReconciliationExecution = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + MultipleDependentActivationCustomResource resource, + Context context) { + + numberOfReconciliationExecution.incrementAndGet(); + + return UpdateControl.noUpdate(); + } + + public int getNumberOfReconciliationExecution() { + return numberOfReconciliationExecution.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/MultipleDependentActivationSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/MultipleDependentActivationSpec.java new file mode 100644 index 0000000000..93bf4b18f3 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/MultipleDependentActivationSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.sample.multipledependentwithactivation; + +public class MultipleDependentActivationSpec { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/SecretDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/SecretDependentResource.java new file mode 100644 index 0000000000..821f5482dc --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/multipledependentwithactivation/SecretDependentResource.java @@ -0,0 +1,31 @@ +package io.javaoperatorsdk.operator.sample.multipledependentwithactivation; + +import java.util.Base64; +import java.util.Map; + +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; + +public class SecretDependentResource + extends CRUDKubernetesDependentResource { + + public SecretDependentResource() { + super(Secret.class); + } + + @Override + protected Secret desired(MultipleDependentActivationCustomResource primary, + Context context) { + // basically does not matter since this should not be called + Secret secret = new Secret(); + secret.setMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + secret.setData(Map.of("data", + Base64.getEncoder().encodeToString(primary.getSpec().getValue().getBytes()))); + return secret; + } +}