diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java index f032c1a05c..e93cd6b046 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java @@ -157,6 +157,13 @@ protected boolean isConditionMet( return condition .map( c -> { + synchronized (this) { + Optional existingResult = + createOrGetResultFor(dependentResource).getConditionResult(c.type()); + if (existingResult.isPresent()) { + return existingResult.get(); + } + } final DetailedCondition.Result r = c.detailedIsMet(dr, primary, context); synchronized (this) { results diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/BaseWorkflowResult.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/BaseWorkflowResult.java index acc304cc79..93a789342b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/BaseWorkflowResult.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/BaseWorkflowResult.java @@ -172,6 +172,15 @@ public boolean hasPostDeleteConditionNotMet() { return deletePostconditionResult != null && !deletePostconditionResult.isSuccess(); } + public Optional getConditionResult(Condition.Type conditionType) { + return switch (conditionType) { + case ACTIVATION -> Optional.ofNullable(activationConditionResult); + case DELETE -> Optional.ofNullable(deletePostconditionResult); + case READY -> Optional.ofNullable(readyPostconditionResult); + case RECONCILE -> Optional.ofNullable(reconcilePostconditionResult); + }; + } + public boolean isReady() { return readyPostconditionResult == null || readyPostconditionResult.isSuccess(); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java index d0d2435347..d544891aba 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java @@ -100,7 +100,7 @@ private synchronized void handleReconcile(DependentResourceNode depend isConditionMet(dependentResourceNode.getReconcilePrecondition(), dependentResourceNode); } if (!reconcileConditionMet || !activationConditionMet) { - handleReconcileOrActivationConditionNotMet(dependentResourceNode, activationConditionMet); + handleReconcileOrActivationConditionNotMet(dependentResourceNode); } else { submit(dependentResourceNode, new NodeReconcileExecutor<>(dependentResourceNode), RECONCILE); } @@ -183,7 +183,10 @@ private NodeDeleteExecutor(DependentResourceNode dependentResourceNode) { @SuppressWarnings("unchecked") protected void doRun(DependentResourceNode dependentResourceNode) { boolean deletePostConditionMet = true; - if (isConditionMet(dependentResourceNode.getActivationCondition(), dependentResourceNode)) { + var active = + isConditionMet(dependentResourceNode.getActivationCondition(), dependentResourceNode); + registerOrDeregisterEventSourceBasedOnActivation(active, dependentResourceNode); + if (active) { // GarbageCollected status is irrelevant here, as this method is only called when a // precondition does not hold, // a deleter should be deleted even if it is otherwise garbage collected @@ -194,7 +197,6 @@ protected void doRun(DependentResourceNode dependentResourceNode) { deletePostConditionMet = isConditionMet(dependentResourceNode.getDeletePostcondition(), dependentResourceNode); } - createOrGetResultFor(dependentResourceNode).markAsVisited(); if (deletePostConditionMet) { handleDependentDeleted(dependentResourceNode); @@ -232,38 +234,20 @@ private synchronized void handleDependentsReconcile( } private void handleReconcileOrActivationConditionNotMet( - DependentResourceNode dependentResourceNode, boolean activationConditionMet) { + DependentResourceNode dependentResourceNode) { Set bottomNodes = new HashSet<>(); - markDependentsForDelete(dependentResourceNode, bottomNodes, activationConditionMet); + markDependentsForDelete(dependentResourceNode, bottomNodes); bottomNodes.forEach(this::handleDelete); } private void markDependentsForDelete( - DependentResourceNode dependentResourceNode, - Set bottomNodes, - boolean activationConditionMet) { - // this is a check so the activation condition is not evaluated twice, - // so if the activation condition was false, this node is not meant to be deleted. + DependentResourceNode dependentResourceNode, Set bottomNodes) { var dependents = dependentResourceNode.getParents(); - if (activationConditionMet) { - // make sure we register the dependent's event source if it hasn't been added already - // this might be needed in corner cases such as - // https://github.com/operator-framework/java-operator-sdk/issues/3249 - registerOrDeregisterEventSourceBasedOnActivation(true, dependentResourceNode); - createOrGetResultFor(dependentResourceNode).markForDelete(); - if (dependents.isEmpty()) { - bottomNodes.add(dependentResourceNode); - } else { - dependents.forEach(d -> markDependentsForDelete(d, bottomNodes, true)); - } + createOrGetResultFor(dependentResourceNode).markForDelete(); + if (dependents.isEmpty()) { + bottomNodes.add(dependentResourceNode); } else { - // this is for an edge case when there is only one resource but that is not active - createOrGetResultFor(dependentResourceNode).markAsVisited(); - if (dependents.isEmpty()) { - handleNodeExecutionFinish(dependentResourceNode); - } else { - dependents.forEach(d -> markDependentsForDelete(d, bottomNodes, true)); - } + dependents.forEach(d -> markDependentsForDelete(d, bottomNodes)); } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutorTest.java index 317df8831f..117fe2afb8 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutorTest.java @@ -45,6 +45,7 @@ class WorkflowReconcileExecutorTest extends AbstractWorkflowExecutorTest { Context mockContext = spy(Context.class); ExecutorService executorService = Executors.newCachedThreadPool(); + EventSourceRetriever eventSourceRetriever = mock(EventSourceRetriever.class); TestDependent dr3 = new TestDependent("DR_3"); TestDependent dr4 = new TestDependent("DR_4"); @@ -56,7 +57,7 @@ void setup(TestInfo testInfo) { when(mockContext.managedWorkflowAndDependentResourceContext()) .thenReturn(mock(ManagedWorkflowAndDependentResourceContext.class)); when(mockContext.getWorkflowExecutorService()).thenReturn(executorService); - when(mockContext.eventSourceRetriever()).thenReturn(mock(EventSourceRetriever.class)); + when(mockContext.eventSourceRetriever()).thenReturn(eventSourceRetriever); } @Test @@ -693,6 +694,52 @@ void activationConditionOnlyCalledOnceOnDeleteDependents() { verify(condition, times(1)).isMet(any(), any(), any()); } + @Test + @SuppressWarnings("unchecked") + void activationConditionEventSourceRegistrationWithParentWithFalsePrecondition() { + var workflow = + new WorkflowBuilder() + .addDependentResourceAndConfigure(dr1) + .withReconcilePrecondition(notMetCondition) + .addDependentResourceAndConfigure(drDeleter) + .withActivationCondition(notMetCondition) + .dependsOn(dr1) + .build(); + + workflow.reconcile(new TestCustomResource(), mockContext); + assertThat(executionHistory).notReconciled(dr1, drDeleter); + verify(eventSourceRetriever, never()).dynamicallyRegisterEventSource(any()); + verify(eventSourceRetriever, times(1)).dynamicallyDeRegisterEventSource(any()); + } + + @Test + @SuppressWarnings("unchecked") + void activationConditionEventSourceRegistration() { + var workflow = + new WorkflowBuilder() + .addDependentResourceAndConfigure(dr1) + .addDependentResourceAndConfigure(drDeleter) + .withActivationCondition(notMetCondition) + .dependsOn(dr1) + .build(); + + workflow.reconcile(new TestCustomResource(), mockContext); + assertThat(executionHistory).reconciled(dr1).notReconciled(drDeleter); + verify(eventSourceRetriever, never()).dynamicallyRegisterEventSource(any()); + verify(eventSourceRetriever, atLeast(1)).dynamicallyDeRegisterEventSource(any()); + } + + @Test + void oneDependentWithActivationCondition() { + var workflow = + new WorkflowBuilder() + .addDependentResourceAndConfigure(dr1) + .withActivationCondition(notMetCondition) + .build(); + workflow.reconcile(new TestCustomResource(), mockContext); + assertThat(executionHistory).notReconciled(dr1); + } + @Test void resultFromReadyConditionShouldBeAvailableIfExisting() { final var result = Integer.valueOf(42); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/ConfigMapDependentResource.java new file mode 100644 index 0000000000..95af1d79cb --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/ConfigMapDependentResource.java @@ -0,0 +1,39 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.onedependentactivationcondition; + +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.CRUDKubernetesDependentResource; + +public class ConfigMapDependentResource + extends CRUDKubernetesDependentResource< + ConfigMap, OneDependentActivationConditionCustomResource> { + + @Override + protected ConfigMap desired( + OneDependentActivationConditionCustomResource primary, + Context context) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata( + new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + return configMap; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/FalseActivationCondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/FalseActivationCondition.java new file mode 100644 index 0000000000..5ec221ff9c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/FalseActivationCondition.java @@ -0,0 +1,32 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.onedependentactivationcondition; + +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; + +public class FalseActivationCondition + implements Condition { + @Override + public boolean isMet( + DependentResource dependentResource, + OneDependentActivationConditionCustomResource primary, + Context context) { + return false; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionCustomResource.java new file mode 100644 index 0000000000..b07526907d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionCustomResource.java @@ -0,0 +1,28 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.onedependentactivationcondition; + +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("odac") +public class OneDependentActivationConditionCustomResource extends CustomResource + implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionIT.java new file mode 100644 index 0000000000..3c67db73ee --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionIT.java @@ -0,0 +1,62 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.onedependentactivationcondition; + +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 static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class OneDependentActivationConditionIT { + + public static final String TEST_RESOURCE_NAME = "test1"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(OneDependentActivationConditionReconciler.class) + .build(); + + @Test + void handlesOnlyNonActiveDependent() { + extension.create(testResource()); + + await() + .untilAsserted( + () -> { + assertThat( + extension + .getReconcilerOfType(OneDependentActivationConditionReconciler.class) + .getNumberOfReconciliationExecution()) + .isPositive(); + }); + + // ConfigMap should not be created since the activation condition is false + var cm = extension.get(ConfigMap.class, TEST_RESOURCE_NAME); + assertThat(cm).isNull(); + } + + private OneDependentActivationConditionCustomResource testResource() { + var res = new OneDependentActivationConditionCustomResource(); + res.setMetadata(new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).build()); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionReconciler.java new file mode 100644 index 0000000000..841c8d23ab --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionReconciler.java @@ -0,0 +1,50 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.onedependentactivationcondition; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Workflow; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +@Workflow( + dependents = { + @Dependent( + type = ConfigMapDependentResource.class, + activationCondition = FalseActivationCondition.class) + }) +@ControllerConfiguration +public class OneDependentActivationConditionReconciler + implements Reconciler { + + private final AtomicInteger numberOfReconciliationExecution = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + OneDependentActivationConditionCustomResource 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/workflow/preconditionandactivation/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/ConfigMapDependentResource.java new file mode 100644 index 0000000000..df6cce54e5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/ConfigMapDependentResource.java @@ -0,0 +1,38 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.preconditionandactivation; + +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.CRUDKubernetesDependentResource; + +public class ConfigMapDependentResource + extends CRUDKubernetesDependentResource { + + @Override + protected ConfigMap desired( + PreconditionAndActivationCustomResource primary, + Context context) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata( + new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + return configMap; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/FalseActivationCondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/FalseActivationCondition.java new file mode 100644 index 0000000000..41a3186ef6 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/FalseActivationCondition.java @@ -0,0 +1,32 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.preconditionandactivation; + +import io.fabric8.kubernetes.api.model.Secret; +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 FalseActivationCondition + implements Condition { + @Override + public boolean isMet( + DependentResource dependentResource, + PreconditionAndActivationCustomResource primary, + Context context) { + return false; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/FalseReconcilePrecondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/FalseReconcilePrecondition.java new file mode 100644 index 0000000000..b3d5771cc9 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/FalseReconcilePrecondition.java @@ -0,0 +1,32 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.preconditionandactivation; + +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; + +public class FalseReconcilePrecondition + implements Condition { + @Override + public boolean isMet( + DependentResource dependentResource, + PreconditionAndActivationCustomResource primary, + Context context) { + return false; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationCustomResource.java new file mode 100644 index 0000000000..41b210bc3b --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationCustomResource.java @@ -0,0 +1,28 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.preconditionandactivation; + +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("paa") +public class PreconditionAndActivationCustomResource extends CustomResource + implements Namespaced {} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationIT.java new file mode 100644 index 0000000000..b9ad2c8370 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationIT.java @@ -0,0 +1,65 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.preconditionandactivation; + +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 static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +class PreconditionAndActivationIT { + + public static final String TEST_RESOURCE_NAME = "test1"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(PreconditionAndActivationReconciler.class) + .build(); + + @Test + void handlesFalsePreconditionWithNonActive() { + extension.create(testResource()); + + await() + .untilAsserted( + () -> { + assertThat( + extension + .getReconcilerOfType(PreconditionAndActivationReconciler.class) + .getNumberOfReconciliationExecution()) + .isPositive(); + }); + + // Neither resource should be created + var cm = extension.get(ConfigMap.class, TEST_RESOURCE_NAME); + assertThat(cm).isNull(); + var secret = extension.get(Secret.class, TEST_RESOURCE_NAME); + assertThat(secret).isNull(); + } + + private PreconditionAndActivationCustomResource testResource() { + var res = new PreconditionAndActivationCustomResource(); + res.setMetadata(new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).build()); + return res; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationReconciler.java new file mode 100644 index 0000000000..a027e63d7f --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationReconciler.java @@ -0,0 +1,67 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.preconditionandactivation; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Workflow; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +@Workflow( + dependents = { + @Dependent( + name = "configmap", + type = ConfigMapDependentResource.class, + reconcilePrecondition = FalseReconcilePrecondition.class), + @Dependent( + type = SecretDependentResource.class, + activationCondition = FalseActivationCondition.class, + dependsOn = "configmap") + }) +@ControllerConfiguration +public class PreconditionAndActivationReconciler + implements Reconciler { + + private final AtomicInteger numberOfReconciliationExecution = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + PreconditionAndActivationCustomResource resource, + Context context) { + + var workflowResult = + context + .managedWorkflowAndDependentResourceContext() + .getWorkflowReconcileResult() + .orElseThrow(); + var erroredDependents = workflowResult.getErroredDependents(); + if (!erroredDependents.isEmpty()) { + throw new RuntimeException( + "Unexpected workflow error", erroredDependents.values().iterator().next()); + } + + numberOfReconciliationExecution.incrementAndGet(); + return UpdateControl.noUpdate(); + } + + public int getNumberOfReconciliationExecution() { + return numberOfReconciliationExecution.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/SecretDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/SecretDependentResource.java new file mode 100644 index 0000000000..d6a5cc2b6d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/SecretDependentResource.java @@ -0,0 +1,38 @@ +/* + * Copyright Java Operator SDK Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.javaoperatorsdk.operator.workflow.preconditionandactivation; + +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 { + + @Override + protected Secret desired( + PreconditionAndActivationCustomResource primary, + Context context) { + Secret secret = new Secret(); + secret.setMetadata( + new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + return secret; + } +}