Skip to content

Commit e7bdd24

Browse files
committed
feat: allow return additional information for conditions
Fixes #2424. Currently, only exposed for ready post conditions but is supported for all conditions if needed. Signed-off-by: Chris Laprun <[email protected]>
1 parent cbf4fad commit e7bdd24

File tree

6 files changed

+169
-37
lines changed

6 files changed

+169
-37
lines changed

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ public boolean isEmpty() {
140140
return dependentResourceNodes.isEmpty();
141141
}
142142

143+
@Override
144+
public int size() {
145+
return dependentResourceNodes.size();
146+
}
147+
143148
@Override
144149
public Map<String, DependentResource> getDependentResourcesByName() {
145150
final var resources = new HashMap<String, DependentResource>(dependentResourceNodes.size());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.javaoperatorsdk.operator.processing.dependent.workflow;
2+
3+
import io.fabric8.kubernetes.api.model.HasMetadata;
4+
import io.javaoperatorsdk.operator.api.reconciler.Context;
5+
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
6+
7+
public interface ResultCondition<R, P extends HasMetadata, T> extends Condition<R, P> {
8+
Result<T> detailedIsMet(DependentResource<R, P> dependentResource, P primary, Context<P> context);
9+
10+
Object NULL = new Object();
11+
12+
@Override
13+
default boolean isMet(DependentResource<R, P> dependentResource, P primary, Context<P> context) {
14+
return detailedIsMet(dependentResource, primary, context).isSuccess();
15+
}
16+
17+
interface Result<T> {
18+
T getResult();
19+
20+
boolean isSuccess();
21+
}
22+
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ default boolean hasCleaner() {
3636
}
3737

3838
default boolean isEmpty() {
39-
return true;
39+
return size() == 0;
40+
}
41+
42+
default int size() {
43+
return getDependentResourcesByName().size();
4044
}
4145

4246
@SuppressWarnings("rawtypes")

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

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.javaoperatorsdk.operator.processing.dependent.workflow;
22

3+
import java.util.HashMap;
34
import java.util.HashSet;
45
import java.util.Map;
56
import java.util.Set;
@@ -22,19 +23,18 @@ public class WorkflowReconcileExecutor<P extends HasMetadata> extends AbstractWo
2223
private static final String RECONCILE = "reconcile";
2324
private static final String DELETE = "delete";
2425

25-
26-
private final Set<DependentResourceNode> notReady = ConcurrentHashMap.newKeySet();
27-
28-
private final Set<DependentResourceNode> markedForDelete = ConcurrentHashMap.newKeySet();
29-
private final Set<DependentResourceNode> deletePostConditionNotMet =
30-
ConcurrentHashMap.newKeySet();
31-
// used to remember reconciled (not deleted or errored) dependents
32-
private final Set<DependentResourceNode> reconciled = ConcurrentHashMap.newKeySet();
33-
private final Map<DependentResource, ReconcileResult> reconcileResults =
34-
new ConcurrentHashMap<>();
26+
private final Map<DependentResourceNode, Object> notReady;
27+
private final Set<DependentResourceNode> markedForDelete;
28+
private final Set<DependentResourceNode> deletePostConditionNotMet;
29+
private final Map<DependentResource, ReconcileResult> reconcileResults;
3530

3631
public WorkflowReconcileExecutor(Workflow<P> workflow, P primary, Context<P> context) {
3732
super(workflow, primary, context);
33+
final var size = workflow.size();
34+
reconcileResults = new HashMap<>(size);
35+
notReady = new HashMap<>(size);
36+
markedForDelete = ConcurrentHashMap.newKeySet(size);
37+
deletePostConditionNotMet = ConcurrentHashMap.newKeySet(size);
3838
}
3939

4040
public synchronized WorkflowReconcileResult reconcile() {
@@ -96,16 +96,16 @@ private synchronized void handleDelete(DependentResourceNode dependentResourceNo
9696

9797
private boolean allDependentsDeletedAlready(DependentResourceNode<?, P> dependentResourceNode) {
9898
var dependents = dependentResourceNode.getParents();
99-
return dependents.stream().allMatch(d -> alreadyVisited(d) && !notReady.contains(d)
99+
return dependents.stream().allMatch(d -> alreadyVisited(d) && !notReady.containsKey(d)
100100
&& !isInError(d) && !deletePostConditionNotMet.contains(d));
101101
}
102102

103103
// needs to be in one step
104104
private synchronized void setAlreadyReconciledButNotReady(
105-
DependentResourceNode<?, P> dependentResourceNode) {
105+
DependentResourceNode<?, P> dependentResourceNode, Object result) {
106106
log.debug("Setting already reconciled but not ready for: {}", dependentResourceNode);
107107
markAsVisited(dependentResourceNode);
108-
notReady.add(dependentResourceNode);
108+
notReady.put(dependentResourceNode, result == null ? ResultCondition.NULL : result);
109109
}
110110

111111
private class NodeReconcileExecutor<R> extends NodeExecutor<R, P> {
@@ -122,17 +122,25 @@ protected void doRun(DependentResourceNode<R, P> dependentResourceNode,
122122
"Reconciling for primary: {} node: {} ", primaryID, dependentResourceNode);
123123
ReconcileResult reconcileResult = dependentResource.reconcile(primary, context);
124124
reconcileResults.put(dependentResource, reconcileResult);
125-
reconciled.add(dependentResourceNode);
126125

127-
boolean ready = isConditionMet(dependentResourceNode.getReadyPostcondition(),
128-
dependentResource);
129-
if (ready) {
126+
final Object[] result = new Object[1];
127+
final var shouldProceed = dependentResourceNode.getReadyPostcondition().map(rpCondition -> {
128+
if (rpCondition instanceof ResultCondition resultCondition) {
129+
final var detailed = resultCondition.detailedIsMet(dependentResource, primary, context);
130+
result[0] = detailed.getResult();
131+
return detailed.isSuccess();
132+
} else {
133+
return rpCondition.isMet(dependentResource, primary, context);
134+
}
135+
}).orElse(true);
136+
137+
if (shouldProceed) {
130138
log.debug("Setting already reconciled for: {} primaryID: {}",
131139
dependentResourceNode, primaryID);
132140
markAsVisited(dependentResourceNode);
133141
handleDependentsReconcile(dependentResourceNode);
134142
} else {
135-
setAlreadyReconciledButNotReady(dependentResourceNode);
143+
setAlreadyReconciledButNotReady(dependentResourceNode, result[0]);
136144
}
137145
}
138146
}
@@ -191,7 +199,6 @@ private synchronized void handleDependentsReconcile(
191199
});
192200
}
193201

194-
195202
private void handleReconcileOrActivationConditionNotMet(
196203
DependentResourceNode<?, P> dependentResourceNode,
197204
boolean activationConditionMet) {
@@ -226,7 +233,7 @@ private void markDependentsForDelete(DependentResourceNode<?, P> dependentResour
226233
private boolean allParentsReconciledAndReady(DependentResourceNode<?, ?> dependentResourceNode) {
227234
return dependentResourceNode.getDependsOn().isEmpty()
228235
|| dependentResourceNode.getDependsOn().stream()
229-
.allMatch(d -> alreadyVisited(d) && !notReady.contains(d));
236+
.allMatch(d -> alreadyVisited(d) && !notReady.containsKey(d));
230237
}
231238

232239
private boolean hasErroredParent(DependentResourceNode<?, ?> dependentResourceNode) {
@@ -237,14 +244,10 @@ private boolean hasErroredParent(DependentResourceNode<?, ?> dependentResourceNo
237244

238245
private WorkflowReconcileResult createReconcileResult() {
239246
return new WorkflowReconcileResult(
240-
reconciled.stream()
241-
.map(DependentResourceNode::getDependentResource)
242-
.collect(Collectors.toList()),
243-
notReady.stream()
244-
.map(DependentResourceNode::getDependentResource)
245-
.collect(Collectors.toList()),
247+
notReady.entrySet().stream()
248+
.collect(Collectors.toMap(entry -> entry.getKey().getDependentResource(),
249+
Map.Entry::getValue)),
246250
getErroredDependents(),
247251
reconcileResults);
248252
}
249-
250253
}

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

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,54 @@
22

33
import java.util.List;
44
import java.util.Map;
5+
import java.util.Optional;
56

67
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
78
import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult;
89

910
@SuppressWarnings("rawtypes")
1011
public class WorkflowReconcileResult extends WorkflowResult {
11-
12-
private final List<DependentResource> reconciledDependents;
13-
private final List<DependentResource> notReadyDependents;
12+
private final Map<DependentResource, Object> notReadyDependents;
1413
private final Map<DependentResource, ReconcileResult> reconcileResults;
1514

16-
public WorkflowReconcileResult(List<DependentResource> reconciledDependents,
17-
List<DependentResource> notReadyDependents,
15+
public WorkflowReconcileResult(
16+
Map<DependentResource, Object> notReadyDependents,
1817
Map<DependentResource, Exception> erroredDependents,
1918
Map<DependentResource, ReconcileResult> reconcileResults) {
2019
super(erroredDependents);
21-
this.reconciledDependents = reconciledDependents;
2220
this.notReadyDependents = notReadyDependents;
2321
this.reconcileResults = reconcileResults;
2422
}
2523

2624
public List<DependentResource> getReconciledDependents() {
27-
return reconciledDependents;
25+
return reconcileResults.keySet().stream().toList();
2826
}
2927

3028
public List<DependentResource> getNotReadyDependents() {
29+
return notReadyDependents.keySet().stream().toList();
30+
}
31+
32+
@SuppressWarnings("unused")
33+
public Map<DependentResource, Object> getNotReadyDependentsWithDetails() {
3134
return notReadyDependents;
3235
}
3336

37+
public <T> T getNotReadyDependentResult(DependentResource dependentResource,
38+
Class<T> expectedResultType) {
39+
final var result = new Object[1];
40+
try {
41+
return Optional.ofNullable(notReadyDependents.get(dependentResource))
42+
.filter(cr -> !ResultCondition.NULL.equals(cr))
43+
.map(r -> result[0] = r)
44+
.map(expectedResultType::cast)
45+
.orElse(null);
46+
} catch (Exception e) {
47+
throw new IllegalArgumentException("Condition result " + result[0] +
48+
" for Dependent " + dependentResource.name() + " doesn't match expected type "
49+
+ expectedResultType.getSimpleName(), e);
50+
}
51+
}
52+
3453
@SuppressWarnings("unused")
3554
public Map<DependentResource, ReconcileResult> getReconcileResults() {
3655
return reconcileResults;

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

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@
77
import org.junit.jupiter.api.BeforeEach;
88
import org.junit.jupiter.api.Test;
99

10+
import io.fabric8.kubernetes.api.model.HasMetadata;
1011
import io.javaoperatorsdk.operator.AggregatedOperatorException;
1112
import io.javaoperatorsdk.operator.api.reconciler.Context;
13+
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
1214
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
1315
import io.javaoperatorsdk.operator.sample.simple.TestCustomResource;
1416

1517
import static io.javaoperatorsdk.operator.processing.dependent.workflow.ExecutionAssert.assertThat;
16-
import static org.junit.jupiter.api.Assertions.assertThrows;
18+
import static org.junit.jupiter.api.Assertions.*;
1719
import static org.mockito.Mockito.*;
1820

19-
@SuppressWarnings("rawtypes")
2021
class WorkflowReconcileExecutorTest extends AbstractWorkflowExecutorTest {
2122

2223
@SuppressWarnings("unchecked")
@@ -27,6 +28,7 @@ class WorkflowReconcileExecutorTest extends AbstractWorkflowExecutorTest {
2728
TestDependent dr4 = new TestDependent("DR_4");
2829

2930
@BeforeEach
31+
@SuppressWarnings("unchecked")
3032
void setup() {
3133
when(mockContext.getWorkflowExecutorService()).thenReturn(executorService);
3234
when(mockContext.eventSourceRetriever()).thenReturn(mock(EventSourceRetriever.class));
@@ -521,6 +523,7 @@ void readyConditionNotCheckedOnNonActiveDependent() {
521523
}
522524

523525
@Test
526+
@SuppressWarnings("unchecked")
524527
void reconcilePreconditionNotCheckedOnNonActiveDependent() {
525528
var precondition = mock(Condition.class);
526529

@@ -557,6 +560,7 @@ void deletesDependentsOfNonActiveDependentButNotTheNonActive() {
557560
}
558561

559562
@Test
563+
@SuppressWarnings("unchecked")
560564
void activationConditionOnlyCalledOnceOnDeleteDependents() {
561565
TestDeleterDependent drDeleter2 = new TestDeleterDependent("DR_DELETER_2");
562566
var condition = mock(Condition.class);
@@ -573,4 +577,79 @@ void activationConditionOnlyCalledOnceOnDeleteDependents() {
573577
verify(condition, times(1)).isMet(any(), any(), any());
574578
}
575579

580+
581+
@Test
582+
void resultFromReadyConditionShouldBeAvailableIfExisting() {
583+
final var result = Integer.valueOf(42);
584+
final var resultCondition = new ResultCondition<>() {
585+
@Override
586+
public Result<Object> detailedIsMet(DependentResource<Object, HasMetadata> dependentResource,
587+
HasMetadata primary, Context<HasMetadata> context) {
588+
return new Result<>() {
589+
@Override
590+
public Object getResult() {
591+
return result;
592+
}
593+
594+
@Override
595+
public boolean isSuccess() {
596+
return false; // force not ready
597+
}
598+
};
599+
}
600+
};
601+
var workflow = new WorkflowBuilder<TestCustomResource>()
602+
.addDependentResource(dr1)
603+
.withReadyPostcondition(resultCondition)
604+
.build();
605+
606+
final var reconcileResult = workflow.reconcile(new TestCustomResource(), mockContext);
607+
assertEquals(result, reconcileResult.getNotReadyDependentResult(dr1, Integer.class));
608+
}
609+
610+
@Test
611+
void shouldThrowIllegalArgumentExceptionIfTypesDoNotMatch() {
612+
final var result = "FOO";
613+
final var resultCondition = new ResultCondition<>() {
614+
@Override
615+
public Result<Object> detailedIsMet(DependentResource<Object, HasMetadata> dependentResource,
616+
HasMetadata primary, Context<HasMetadata> context) {
617+
return new Result<>() {
618+
@Override
619+
public Object getResult() {
620+
return result;
621+
}
622+
623+
@Override
624+
public boolean isSuccess() {
625+
return false; // force not ready
626+
}
627+
};
628+
}
629+
};
630+
var workflow = new WorkflowBuilder<TestCustomResource>()
631+
.addDependentResource(dr1)
632+
.withReadyPostcondition(resultCondition)
633+
.build();
634+
635+
final var reconcileResult = workflow.reconcile(new TestCustomResource(), mockContext);
636+
final var expectedResultType = Integer.class;
637+
final var e = assertThrows(IllegalArgumentException.class,
638+
() -> reconcileResult.getNotReadyDependentResult(dr1, expectedResultType));
639+
final var message = e.getMessage();
640+
assertTrue(message.contains(dr1.name()));
641+
assertTrue(message.contains(expectedResultType.getSimpleName()));
642+
assertTrue(message.contains(result));
643+
}
644+
645+
@Test
646+
void shouldReturnNullIfNoConditionResultExists() {
647+
var workflow = new WorkflowBuilder<TestCustomResource>()
648+
.addDependentResource(dr1)
649+
.withReadyPostcondition(notMetCondition)
650+
.build();
651+
652+
final var reconcileResult = workflow.reconcile(new TestCustomResource(), mockContext);
653+
assertNull(reconcileResult.getNotReadyDependentResult(dr1, Integer.class));
654+
}
576655
}

0 commit comments

Comments
 (0)