Skip to content

Commit c048921

Browse files
authored
fix: handling delete non-active dependent when parent reconcile condition false (#3273)
Signed-off-by: Attila Mészáros <a_meszaros@apple.com>
1 parent e9053ee commit c048921

File tree

17 files changed

+611
-29
lines changed

17 files changed

+611
-29
lines changed

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ protected <R> boolean isConditionMet(
157157
return condition
158158
.map(
159159
c -> {
160+
Optional<DetailedCondition.Result> existingResult =
161+
createOrGetResultFor(dependentResource).getConditionResult(c.type());
162+
if (existingResult.isPresent()) {
163+
return existingResult.get();
164+
}
160165
final DetailedCondition.Result<?> r = c.detailedIsMet(dr, primary, context);
161166
synchronized (this) {
162167
results

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/BaseWorkflowResult.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,15 @@ public boolean hasPostDeleteConditionNotMet() {
172172
return deletePostconditionResult != null && !deletePostconditionResult.isSuccess();
173173
}
174174

175+
public Optional<DetailedCondition.Result> getConditionResult(Condition.Type conditionType) {
176+
return switch (conditionType) {
177+
case ACTIVATION -> Optional.ofNullable(activationConditionResult);
178+
case DELETE -> Optional.ofNullable(deletePostconditionResult);
179+
case READY -> Optional.ofNullable(readyPostconditionResult);
180+
case RECONCILE -> Optional.ofNullable(reconcilePostconditionResult);
181+
};
182+
}
183+
175184
public boolean isReady() {
176185
return readyPostconditionResult == null || readyPostconditionResult.isSuccess();
177186
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ private synchronized <R> void handleReconcile(DependentResourceNode<R, P> depend
100100
isConditionMet(dependentResourceNode.getReconcilePrecondition(), dependentResourceNode);
101101
}
102102
if (!reconcileConditionMet || !activationConditionMet) {
103-
handleReconcileOrActivationConditionNotMet(dependentResourceNode, activationConditionMet);
103+
handleReconcileOrActivationConditionNotMet(dependentResourceNode);
104104
} else {
105105
submit(dependentResourceNode, new NodeReconcileExecutor<>(dependentResourceNode), RECONCILE);
106106
}
@@ -183,7 +183,10 @@ private NodeDeleteExecutor(DependentResourceNode<R, P> dependentResourceNode) {
183183
@SuppressWarnings("unchecked")
184184
protected void doRun(DependentResourceNode<R, P> dependentResourceNode) {
185185
boolean deletePostConditionMet = true;
186-
if (isConditionMet(dependentResourceNode.getActivationCondition(), dependentResourceNode)) {
186+
var active =
187+
isConditionMet(dependentResourceNode.getActivationCondition(), dependentResourceNode);
188+
registerOrDeregisterEventSourceBasedOnActivation(active, dependentResourceNode);
189+
if (active) {
187190
// GarbageCollected status is irrelevant here, as this method is only called when a
188191
// precondition does not hold,
189192
// a deleter should be deleted even if it is otherwise garbage collected
@@ -194,7 +197,6 @@ protected void doRun(DependentResourceNode<R, P> dependentResourceNode) {
194197
deletePostConditionMet =
195198
isConditionMet(dependentResourceNode.getDeletePostcondition(), dependentResourceNode);
196199
}
197-
198200
createOrGetResultFor(dependentResourceNode).markAsVisited();
199201
if (deletePostConditionMet) {
200202
handleDependentDeleted(dependentResourceNode);
@@ -232,38 +234,20 @@ private synchronized void handleDependentsReconcile(
232234
}
233235

234236
private void handleReconcileOrActivationConditionNotMet(
235-
DependentResourceNode<?, P> dependentResourceNode, boolean activationConditionMet) {
237+
DependentResourceNode<?, P> dependentResourceNode) {
236238
Set<DependentResourceNode> bottomNodes = new HashSet<>();
237-
markDependentsForDelete(dependentResourceNode, bottomNodes, activationConditionMet);
239+
markDependentsForDelete(dependentResourceNode, bottomNodes);
238240
bottomNodes.forEach(this::handleDelete);
239241
}
240242

241243
private void markDependentsForDelete(
242-
DependentResourceNode<?, P> dependentResourceNode,
243-
Set<DependentResourceNode> bottomNodes,
244-
boolean activationConditionMet) {
245-
// this is a check so the activation condition is not evaluated twice,
246-
// so if the activation condition was false, this node is not meant to be deleted.
244+
DependentResourceNode<?, P> dependentResourceNode, Set<DependentResourceNode> bottomNodes) {
247245
var dependents = dependentResourceNode.getParents();
248-
if (activationConditionMet) {
249-
// make sure we register the dependent's event source if it hasn't been added already
250-
// this might be needed in corner cases such as
251-
// https://github.com/operator-framework/java-operator-sdk/issues/3249
252-
registerOrDeregisterEventSourceBasedOnActivation(true, dependentResourceNode);
253-
createOrGetResultFor(dependentResourceNode).markForDelete();
254-
if (dependents.isEmpty()) {
255-
bottomNodes.add(dependentResourceNode);
256-
} else {
257-
dependents.forEach(d -> markDependentsForDelete(d, bottomNodes, true));
258-
}
246+
createOrGetResultFor(dependentResourceNode).markForDelete();
247+
if (dependents.isEmpty()) {
248+
bottomNodes.add(dependentResourceNode);
259249
} else {
260-
// this is for an edge case when there is only one resource but that is not active
261-
createOrGetResultFor(dependentResourceNode).markAsVisited();
262-
if (dependents.isEmpty()) {
263-
handleNodeExecutionFinish(dependentResourceNode);
264-
} else {
265-
dependents.forEach(d -> markDependentsForDelete(d, bottomNodes, true));
266-
}
250+
dependents.forEach(d -> markDependentsForDelete(d, bottomNodes));
267251
}
268252
}
269253

operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutorTest.java

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class WorkflowReconcileExecutorTest extends AbstractWorkflowExecutorTest {
4545
Context<TestCustomResource> mockContext = spy(Context.class);
4646

4747
ExecutorService executorService = Executors.newCachedThreadPool();
48+
EventSourceRetriever eventSourceRetriever = mock(EventSourceRetriever.class);
4849

4950
TestDependent dr3 = new TestDependent("DR_3");
5051
TestDependent dr4 = new TestDependent("DR_4");
@@ -56,7 +57,7 @@ void setup(TestInfo testInfo) {
5657
when(mockContext.managedWorkflowAndDependentResourceContext())
5758
.thenReturn(mock(ManagedWorkflowAndDependentResourceContext.class));
5859
when(mockContext.getWorkflowExecutorService()).thenReturn(executorService);
59-
when(mockContext.eventSourceRetriever()).thenReturn(mock(EventSourceRetriever.class));
60+
when(mockContext.eventSourceRetriever()).thenReturn(eventSourceRetriever);
6061
}
6162

6263
@Test
@@ -693,6 +694,52 @@ void activationConditionOnlyCalledOnceOnDeleteDependents() {
693694
verify(condition, times(1)).isMet(any(), any(), any());
694695
}
695696

697+
@Test
698+
@SuppressWarnings("unchecked")
699+
void activationConditionEventSourceRegistrationWithParentWithFalsePrecondition() {
700+
var workflow =
701+
new WorkflowBuilder<TestCustomResource>()
702+
.addDependentResourceAndConfigure(dr1)
703+
.withReconcilePrecondition(notMetCondition)
704+
.addDependentResourceAndConfigure(drDeleter)
705+
.withActivationCondition(notMetCondition)
706+
.dependsOn(dr1)
707+
.build();
708+
709+
workflow.reconcile(new TestCustomResource(), mockContext);
710+
assertThat(executionHistory).notReconciled(dr1, drDeleter);
711+
verify(eventSourceRetriever, never()).dynamicallyRegisterEventSource(any());
712+
verify(eventSourceRetriever, times(1)).dynamicallyDeRegisterEventSource(any());
713+
}
714+
715+
@Test
716+
@SuppressWarnings("unchecked")
717+
void activationConditionEventSourceRegistration() {
718+
var workflow =
719+
new WorkflowBuilder<TestCustomResource>()
720+
.addDependentResourceAndConfigure(dr1)
721+
.addDependentResourceAndConfigure(drDeleter)
722+
.withActivationCondition(notMetCondition)
723+
.dependsOn(dr1)
724+
.build();
725+
726+
workflow.reconcile(new TestCustomResource(), mockContext);
727+
assertThat(executionHistory).reconciled(dr1).notReconciled(drDeleter);
728+
verify(eventSourceRetriever, never()).dynamicallyRegisterEventSource(any());
729+
verify(eventSourceRetriever, atLeast(1)).dynamicallyDeRegisterEventSource(any());
730+
}
731+
732+
@Test
733+
void oneDependentWithActivationCondition() {
734+
var workflow =
735+
new WorkflowBuilder<TestCustomResource>()
736+
.addDependentResourceAndConfigure(dr1)
737+
.withActivationCondition(notMetCondition)
738+
.build();
739+
workflow.reconcile(new TestCustomResource(), mockContext);
740+
assertThat(executionHistory).notReconciled(dr1);
741+
}
742+
696743
@Test
697744
void resultFromReadyConditionShouldBeAvailableIfExisting() {
698745
final var result = Integer.valueOf(42);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.workflow.onedependentactivationcondition;
17+
18+
import io.fabric8.kubernetes.api.model.ConfigMap;
19+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
20+
import io.javaoperatorsdk.operator.api.reconciler.Context;
21+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource;
22+
23+
public class ConfigMapDependentResource
24+
extends CRUDKubernetesDependentResource<
25+
ConfigMap, OneDependentActivationConditionCustomResource> {
26+
27+
@Override
28+
protected ConfigMap desired(
29+
OneDependentActivationConditionCustomResource primary,
30+
Context<OneDependentActivationConditionCustomResource> context) {
31+
ConfigMap configMap = new ConfigMap();
32+
configMap.setMetadata(
33+
new ObjectMetaBuilder()
34+
.withName(primary.getMetadata().getName())
35+
.withNamespace(primary.getMetadata().getNamespace())
36+
.build());
37+
return configMap;
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.workflow.onedependentactivationcondition;
17+
18+
import io.fabric8.kubernetes.api.model.ConfigMap;
19+
import io.javaoperatorsdk.operator.api.reconciler.Context;
20+
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
21+
import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition;
22+
23+
public class FalseActivationCondition
24+
implements Condition<ConfigMap, OneDependentActivationConditionCustomResource> {
25+
@Override
26+
public boolean isMet(
27+
DependentResource<ConfigMap, OneDependentActivationConditionCustomResource> dependentResource,
28+
OneDependentActivationConditionCustomResource primary,
29+
Context<OneDependentActivationConditionCustomResource> context) {
30+
return false;
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.workflow.onedependentactivationcondition;
17+
18+
import io.fabric8.kubernetes.api.model.Namespaced;
19+
import io.fabric8.kubernetes.client.CustomResource;
20+
import io.fabric8.kubernetes.model.annotation.Group;
21+
import io.fabric8.kubernetes.model.annotation.ShortNames;
22+
import io.fabric8.kubernetes.model.annotation.Version;
23+
24+
@Group("sample.javaoperatorsdk")
25+
@Version("v1")
26+
@ShortNames("odac")
27+
public class OneDependentActivationConditionCustomResource extends CustomResource<Void, Void>
28+
implements Namespaced {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.workflow.onedependentactivationcondition;
17+
18+
import org.junit.jupiter.api.Test;
19+
import org.junit.jupiter.api.extension.RegisterExtension;
20+
21+
import io.fabric8.kubernetes.api.model.ConfigMap;
22+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
23+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
import static org.awaitility.Awaitility.await;
27+
28+
class OneDependentActivationConditionIT {
29+
30+
public static final String TEST_RESOURCE_NAME = "test1";
31+
32+
@RegisterExtension
33+
LocallyRunOperatorExtension extension =
34+
LocallyRunOperatorExtension.builder()
35+
.withReconciler(OneDependentActivationConditionReconciler.class)
36+
.build();
37+
38+
@Test
39+
void handlesOnlyNonActiveDependent() {
40+
extension.create(testResource());
41+
42+
await()
43+
.untilAsserted(
44+
() -> {
45+
assertThat(
46+
extension
47+
.getReconcilerOfType(OneDependentActivationConditionReconciler.class)
48+
.getNumberOfReconciliationExecution())
49+
.isPositive();
50+
});
51+
52+
// ConfigMap should not be created since the activation condition is false
53+
var cm = extension.get(ConfigMap.class, TEST_RESOURCE_NAME);
54+
assertThat(cm).isNull();
55+
}
56+
57+
private OneDependentActivationConditionCustomResource testResource() {
58+
var res = new OneDependentActivationConditionCustomResource();
59+
res.setMetadata(new ObjectMetaBuilder().withName(TEST_RESOURCE_NAME).build());
60+
return res;
61+
}
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright Java Operator SDK Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.javaoperatorsdk.operator.workflow.onedependentactivationcondition;
17+
18+
import java.util.concurrent.atomic.AtomicInteger;
19+
20+
import io.javaoperatorsdk.operator.api.reconciler.Context;
21+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
22+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
23+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
24+
import io.javaoperatorsdk.operator.api.reconciler.Workflow;
25+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
26+
27+
@Workflow(
28+
dependents = {
29+
@Dependent(
30+
type = ConfigMapDependentResource.class,
31+
activationCondition = FalseActivationCondition.class)
32+
})
33+
@ControllerConfiguration
34+
public class OneDependentActivationConditionReconciler
35+
implements Reconciler<OneDependentActivationConditionCustomResource> {
36+
37+
private final AtomicInteger numberOfReconciliationExecution = new AtomicInteger(0);
38+
39+
@Override
40+
public UpdateControl<OneDependentActivationConditionCustomResource> reconcile(
41+
OneDependentActivationConditionCustomResource resource,
42+
Context<OneDependentActivationConditionCustomResource> context) {
43+
numberOfReconciliationExecution.incrementAndGet();
44+
return UpdateControl.noUpdate();
45+
}
46+
47+
public int getNumberOfReconciliationExecution() {
48+
return numberOfReconciliationExecution.get();
49+
}
50+
}

0 commit comments

Comments
 (0)