From ea8466583fbfdb421208dfa1a51fed8ea45edd58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 7 Apr 2026 12:24:03 +0200 Subject: [PATCH 1/7] fix: handling delete non-active dependent when parent reconcile condition false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../dependent/workflow/WorkflowReconcileExecutor.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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..5d5cd31de8 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 @@ -238,6 +238,13 @@ private void handleReconcileOrActivationConditionNotMet( bottomNodes.forEach(this::handleDelete); } + private void markDependentsForDelete( + DependentResourceNode dependentResourceNode, Set bottomNodes) { + boolean activationConditionMet = + isConditionMet(dependentResourceNode.getActivationCondition(), dependentResourceNode); + markDependentsForDelete(dependentResourceNode, bottomNodes, activationConditionMet); + } + private void markDependentsForDelete( DependentResourceNode dependentResourceNode, Set bottomNodes, @@ -254,7 +261,7 @@ private void markDependentsForDelete( if (dependents.isEmpty()) { bottomNodes.add(dependentResourceNode); } else { - dependents.forEach(d -> markDependentsForDelete(d, bottomNodes, true)); + dependents.forEach(d -> markDependentsForDelete(d, bottomNodes)); } } else { // this is for an edge case when there is only one resource but that is not active @@ -262,7 +269,7 @@ private void markDependentsForDelete( if (dependents.isEmpty()) { handleNodeExecutionFinish(dependentResourceNode); } else { - dependents.forEach(d -> markDependentsForDelete(d, bottomNodes, true)); + dependents.forEach(d -> markDependentsForDelete(d, bottomNodes)); } } } From 461e78ebca7f101c12094cabc6d25ab574e7670b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 7 Apr 2026 13:19:41 +0200 Subject: [PATCH 2/7] Avoiding condition double checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../dependent/workflow/AbstractWorkflowExecutor.java | 7 +++++++ .../dependent/workflow/BaseWorkflowResult.java | 9 +++++++++ .../dependent/workflow/WorkflowReconcileExecutor.java | 5 ++--- 3 files changed, 18 insertions(+), 3 deletions(-) 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 5d5cd31de8..7028eb5ca0 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 @@ -255,7 +255,6 @@ private void markDependentsForDelete( 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()) { @@ -264,11 +263,11 @@ private void markDependentsForDelete( dependents.forEach(d -> markDependentsForDelete(d, bottomNodes)); } } else { - // this is for an edge case when there is only one resource but that is not active - createOrGetResultFor(dependentResourceNode).markAsVisited(); if (dependents.isEmpty()) { + createOrGetResultFor(dependentResourceNode).markAsVisited(); handleNodeExecutionFinish(dependentResourceNode); } else { + createOrGetResultFor(dependentResourceNode).markForDelete(); dependents.forEach(d -> markDependentsForDelete(d, bottomNodes)); } } From ad7c9c639526e34935eb209af81f58adfa556c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 7 Apr 2026 14:16:45 +0200 Subject: [PATCH 3/7] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../workflow/WorkflowReconcileExecutor.java | 46 +++++-------------- .../source/informer/InformerEventSource.java | 3 ++ .../WorkflowReconcileExecutorTest.java | 38 ++++++++++++++- 3 files changed, 52 insertions(+), 35 deletions(-) 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 7028eb5ca0..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,44 +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 = - isConditionMet(dependentResourceNode.getActivationCondition(), dependentResourceNode); - markDependentsForDelete(dependentResourceNode, bottomNodes, activationConditionMet); - } - 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 - registerOrDeregisterEventSourceBasedOnActivation(true, dependentResourceNode); - createOrGetResultFor(dependentResourceNode).markForDelete(); - if (dependents.isEmpty()) { - bottomNodes.add(dependentResourceNode); - } else { - dependents.forEach(d -> markDependentsForDelete(d, bottomNodes)); - } + createOrGetResultFor(dependentResourceNode).markForDelete(); + if (dependents.isEmpty()) { + bottomNodes.add(dependentResourceNode); } else { - if (dependents.isEmpty()) { - createOrGetResultFor(dependentResourceNode).markAsVisited(); - handleNodeExecutionFinish(dependentResourceNode); - } else { - createOrGetResultFor(dependentResourceNode).markForDelete(); - dependents.forEach(d -> markDependentsForDelete(d, bottomNodes)); - } + dependents.forEach(d -> markDependentsForDelete(d, bottomNodes)); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 70d83e640e..93d3eb5e80 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -139,6 +139,9 @@ protected void handleEvent( @Override public synchronized void start() { + if (isRunning()) { + return; + } super.start(); // this makes sure that on first reconciliation all resources are // present on the index 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..444e5848c9 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,41 @@ 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 resultFromReadyConditionShouldBeAvailableIfExisting() { final var result = Integer.valueOf(42); From c8440a81c17761c525c51bc2f764829aed739864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 7 Apr 2026 14:25:36 +0200 Subject: [PATCH 4/7] additional unit test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../workflow/WorkflowReconcileExecutorTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 444e5848c9..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 @@ -729,6 +729,17 @@ void activationConditionEventSourceRegistration() { 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); From 819b0441f04091e900bcfa14c0f1c469643d4660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 7 Apr 2026 15:07:46 +0200 Subject: [PATCH 5/7] add integration test for corner case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../ConfigMapDependentResource.java | 39 ++++++++++++ .../FalseActivationCondition.java | 32 ++++++++++ ...dentActivationConditionCustomResource.java | 28 +++++++++ .../OneDependentActivationConditionIT.java | 62 +++++++++++++++++++ ...ependentActivationConditionReconciler.java | 50 +++++++++++++++ 5 files changed, 211 insertions(+) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/ConfigMapDependentResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/FalseActivationCondition.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/onedependentactivationcondition/OneDependentActivationConditionReconciler.java 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(); + } +} From 2073e65f05fc957d9469b50beec59cd6a87046fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 7 Apr 2026 15:23:49 +0200 Subject: [PATCH 6/7] test case for reported issue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../ConfigMapDependentResource.java | 38 +++++++++++ .../FalseActivationCondition.java | 32 +++++++++ .../FalseReconcilePrecondition.java | 32 +++++++++ ...econditionAndActivationCustomResource.java | 28 ++++++++ .../PreconditionAndActivationIT.java | 65 ++++++++++++++++++ .../PreconditionAndActivationReconciler.java | 67 +++++++++++++++++++ .../SecretDependentResource.java | 38 +++++++++++ 7 files changed, 300 insertions(+) create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/ConfigMapDependentResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/FalseActivationCondition.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/FalseReconcilePrecondition.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationCustomResource.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationIT.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/PreconditionAndActivationReconciler.java create mode 100644 operator-framework/src/test/java/io/javaoperatorsdk/operator/workflow/preconditionandactivation/SecretDependentResource.java 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; + } +} From 79a2b2f68da18a502dac6731277556520bdefdee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Attila=20M=C3=A9sz=C3=A1ros?= Date: Tue, 7 Apr 2026 16:01:22 +0200 Subject: [PATCH 7/7] wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Attila Mészáros --- .../processing/event/source/informer/InformerEventSource.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index 93d3eb5e80..70d83e640e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -139,9 +139,6 @@ protected void handleEvent( @Override public synchronized void start() { - if (isRunning()) { - return; - } super.start(); // this makes sure that on first reconciliation all resources are // present on the index