Skip to content

Commit 88c4b60

Browse files
metacosmcsviri
andcommitted
feat: allow returning additional information from conditions (#2426)
Fixes #2424. --------- Signed-off-by: Attila Mészáros <[email protected]> Signed-off-by: Chris Laprun <[email protected]> Co-authored-by: Attila Mészáros <[email protected]>
1 parent 4bdbeb3 commit 88c4b60

29 files changed

+795
-281
lines changed

Diff for: docs/content/en/docs/workflows/_index.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,26 @@ reconciliation process.
4949
See related [integration test](https://github.com/operator-framework/java-operator-sdk/blob/ba5e33527bf9e3ea0bd33025ccb35e677f9d44b4/operator-framework/src/test/java/io/javaoperatorsdk/operator/CRDPresentActivationConditionIT.java).
5050

5151
To have multiple resources of same type with an activation condition is a bit tricky, since you
52-
don't want to have multiple `InformerEvetnSource` for the same type, you have to explicitly
52+
don't want to have multiple `InformerEventSource` for the same type, you have to explicitly
5353
name the informer for the Dependent Resource (`@KubernetesDependent(informerConfig = @InformerConfig(name = "configMapInformer"))`)
5454
for all resource of same type with activation condition. This will make sure that only one is registered.
5555
See details at [low level api](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java#L20-L52).
5656

57+
### Result conditions
58+
59+
While simple conditions are usually enough, it might happen you want to convey extra information as a result of the
60+
evaluation of the conditions (e.g., to report error messages or because the result of the condition evaluation might be
61+
interesting for other purposes). In this situation, you should implement `DetailedCondition` instead of `Condition` and
62+
provide an implementation of the `detailedIsMet` method, which allows you to return a more detailed `Result` object via
63+
which you can provide extra information. The `DetailedCondition.Result` interface provides factory method for your
64+
convenience but you can also provide your own implementation if required.
65+
66+
You can access the results for conditions from the `WorkflowResult` instance that is returned whenever a workflow is
67+
evaluated. You can access that result from the `ManagedWorkflowAndDependentResourceContext` accessible from the
68+
reconciliation `Context`. You can then access individual condition results using the `
69+
getDependentConditionResult` methods. You can see an example of this
70+
in [this integration test](https://github.com/operator-framework/java-operator-sdk/blob/fd0e92c0de55c47d5df50658cf4e147ee5e6102d/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureReconciler.java#L44-L49).
71+
5772
## Defining Workflows
5873

5974
Similarly to dependent resources, there are two ways to define workflows, in managed and standalone

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Workflow.java

+7-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import io.fabric8.kubernetes.api.model.HasMetadata;
1010
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
11+
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult;
12+
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowReconcileResult;
1113

1214
@Inherited
1315
@Retention(RetentionPolicy.RUNTIME)
@@ -29,12 +31,11 @@
2931
* execution as would normally be the case. Instead, it will proceed to its
3032
* {@link Reconciler#reconcile(HasMetadata, Context)} method as if no error occurred. It is then
3133
* up to the developer to decide how to proceed by retrieving the errored dependents (and their
32-
* associated exception) via
33-
* {@link io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowResult#erroredDependents},
34-
* the workflow result itself being accessed from
35-
* {@link Context#managedWorkflowAndDependentResourceContext()}. If {@code false}, an exception
36-
* will be automatically thrown at the end of the workflow execution, presenting an aggregated
37-
* view of what happened.
34+
* associated exception) via {@link WorkflowReconcileResult#getErroredDependents()} or
35+
* {@link WorkflowCleanupResult#getErroredDependents()}, the workflow result itself being accessed
36+
* from {@link Context#managedWorkflowAndDependentResourceContext()}. If {@code false}, an
37+
* exception will be automatically thrown at the end of the workflow execution, presenting an
38+
* aggregated view of what happened.
3839
*/
3940
boolean handleExceptionsInReconciler() default false;
4041

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

+50-20
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
package io.javaoperatorsdk.operator.processing.dependent.workflow;
22

33
import java.util.Map;
4-
import java.util.Map.Entry;
54
import java.util.Optional;
6-
import java.util.Set;
75
import java.util.concurrent.ConcurrentHashMap;
86
import java.util.concurrent.ExecutorService;
97
import java.util.concurrent.Future;
8+
import java.util.function.Function;
109
import java.util.stream.Collectors;
1110

1211
import org.slf4j.Logger;
@@ -20,25 +19,24 @@
2019
@SuppressWarnings("rawtypes")
2120
abstract class AbstractWorkflowExecutor<P extends HasMetadata> {
2221

23-
protected final Workflow<P> workflow;
22+
protected final DefaultWorkflow<P> workflow;
2423
protected final P primary;
2524
protected final ResourceID primaryID;
2625
protected final Context<P> context;
26+
protected final Map<DependentResourceNode<?, P>, WorkflowResult.DetailBuilder<?>> results;
2727
/**
2828
* Covers both deleted and reconciled
2929
*/
30-
private final Set<DependentResourceNode> alreadyVisited = ConcurrentHashMap.newKeySet();
3130
private final Map<DependentResourceNode, Future<?>> actualExecutions = new ConcurrentHashMap<>();
32-
private final Map<DependentResourceNode, Exception> exceptionsDuringExecution =
33-
new ConcurrentHashMap<>();
3431
private final ExecutorService executorService;
3532

36-
public AbstractWorkflowExecutor(Workflow<P> workflow, P primary, Context<P> context) {
33+
protected AbstractWorkflowExecutor(DefaultWorkflow<P> workflow, P primary, Context<P> context) {
3734
this.workflow = workflow;
3835
this.primary = primary;
3936
this.context = context;
4037
this.primaryID = ResourceID.fromResource(primary);
4138
executorService = context.getWorkflowExecutorService();
39+
results = new ConcurrentHashMap<>(workflow.getDependentResourcesByName().size());
4240
}
4341

4442
protected abstract Logger logger();
@@ -75,11 +73,31 @@ protected boolean noMoreExecutionsScheduled() {
7573
}
7674

7775
protected boolean alreadyVisited(DependentResourceNode<?, P> dependentResourceNode) {
78-
return alreadyVisited.contains(dependentResourceNode);
76+
return getResultFlagFor(dependentResourceNode, WorkflowResult.DetailBuilder::isVisited);
7977
}
8078

81-
protected void markAsVisited(DependentResourceNode<?, P> dependentResourceNode) {
82-
alreadyVisited.add(dependentResourceNode);
79+
protected boolean postDeleteConditionNotMet(DependentResourceNode<?, P> drn) {
80+
return getResultFlagFor(drn, WorkflowResult.DetailBuilder::hasPostDeleteConditionNotMet);
81+
}
82+
83+
protected boolean isMarkedForDelete(DependentResourceNode<?, P> drn) {
84+
return getResultFlagFor(drn, WorkflowResult.DetailBuilder::isMarkedForDelete);
85+
}
86+
87+
protected WorkflowResult.DetailBuilder createOrGetResultFor(
88+
DependentResourceNode<?, P> dependentResourceNode) {
89+
return results.computeIfAbsent(dependentResourceNode,
90+
unused -> new WorkflowResult.DetailBuilder());
91+
}
92+
93+
protected Optional<WorkflowResult.DetailBuilder<?>> getResultFor(
94+
DependentResourceNode<?, P> dependentResourceNode) {
95+
return Optional.ofNullable(results.get(dependentResourceNode));
96+
}
97+
98+
protected boolean getResultFlagFor(DependentResourceNode<?, P> dependentResourceNode,
99+
Function<WorkflowResult.DetailBuilder<?>, Boolean> flag) {
100+
return getResultFor(dependentResourceNode).map(flag).orElse(false);
83101
}
84102

85103
protected boolean isExecutingNow(DependentResourceNode<?, P> dependentResourceNode) {
@@ -94,17 +112,15 @@ protected void markAsExecuting(DependentResourceNode<?, P> dependentResourceNode
94112
protected synchronized void handleExceptionInExecutor(
95113
DependentResourceNode<?, P> dependentResourceNode,
96114
RuntimeException e) {
97-
exceptionsDuringExecution.put(dependentResourceNode, e);
115+
createOrGetResultFor(dependentResourceNode).withError(e);
98116
}
99117

100-
protected boolean isInError(DependentResourceNode<?, P> dependentResourceNode) {
101-
return exceptionsDuringExecution.containsKey(dependentResourceNode);
118+
protected boolean isNotReady(DependentResourceNode<?, P> dependentResourceNode) {
119+
return getResultFlagFor(dependentResourceNode, WorkflowResult.DetailBuilder::isNotReady);
102120
}
103121

104-
protected Map<DependentResource, Exception> getErroredDependents() {
105-
return exceptionsDuringExecution.entrySet().stream()
106-
.collect(
107-
Collectors.toMap(e -> e.getKey().getDependentResource(), Entry::getValue));
122+
protected boolean isInError(DependentResourceNode<?, P> dependentResourceNode) {
123+
return getResultFlagFor(dependentResourceNode, WorkflowResult.DetailBuilder::hasError);
108124
}
109125

110126
protected synchronized void handleNodeExecutionFinish(
@@ -116,9 +132,17 @@ protected synchronized void handleNodeExecutionFinish(
116132
}
117133
}
118134

119-
protected <R> boolean isConditionMet(Optional<Condition<R, P>> condition,
120-
DependentResource<R, P> dependentResource) {
121-
return condition.map(c -> c.isMet(dependentResource, primary, context)).orElse(true);
135+
@SuppressWarnings("unchecked")
136+
protected <R> boolean isConditionMet(
137+
Optional<ConditionWithType<R, P, ?>> condition,
138+
DependentResourceNode<R, P> dependentResource) {
139+
final var dr = dependentResource.getDependentResource();
140+
return condition.map(c -> {
141+
final DetailedCondition.Result<?> r = c.detailedIsMet(dr, primary, context);
142+
results.computeIfAbsent(dependentResource, unused -> new WorkflowResult.DetailBuilder())
143+
.withResultForCondition(c, r);
144+
return r;
145+
}).orElse(DetailedCondition.Result.metWithoutResult).isSuccess();
122146
}
123147

124148
protected <R> void submit(DependentResourceNode<R, P> dependentResourceNode,
@@ -145,4 +169,10 @@ protected <R> void registerOrDeregisterEventSourceBasedOnActivation(
145169
}
146170
}
147171
}
172+
173+
protected Map<DependentResource, WorkflowResult.Detail<?>> asDetails() {
174+
return results.entrySet().stream()
175+
.collect(
176+
Collectors.toMap(e -> e.getKey().getDependentResource(), e -> e.getValue().build()));
177+
}
148178
}

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Condition.java

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
public interface Condition<R, P extends HasMetadata> {
88

9+
enum Type {
10+
ACTIVATION, DELETE, READY, RECONCILE
11+
}
12+
913
/**
1014
* Checks whether a condition holds true for a given
1115
* {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource} based on the
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
class ConditionWithType<R, P extends HasMetadata, T> implements DetailedCondition<R, P, T> {
8+
private final Condition<R, P> condition;
9+
private final Type type;
10+
11+
ConditionWithType(Condition<R, P> condition, Type type) {
12+
this.condition = condition;
13+
this.type = type;
14+
}
15+
16+
public Type type() {
17+
return type;
18+
}
19+
20+
@SuppressWarnings("unchecked")
21+
@Override
22+
public Result<T> detailedIsMet(DependentResource<R, P> dependentResource, P primary,
23+
Context<P> context) {
24+
if (condition instanceof DetailedCondition detailedCondition) {
25+
return detailedCondition.detailedIsMet(dependentResource, primary, context);
26+
} else {
27+
return Result
28+
.withoutResult(condition.isMet(dependentResource, primary, context));
29+
}
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.javaoperatorsdk.operator.processing.dependent.workflow;
2+
3+
public class DefaultResult<T> implements DetailedCondition.Result<T> {
4+
private final T result;
5+
private final boolean success;
6+
7+
public DefaultResult(boolean success, T result) {
8+
this.result = result;
9+
this.success = success;
10+
}
11+
12+
@Override
13+
public T getDetail() {
14+
return result;
15+
}
16+
17+
@Override
18+
public boolean isSuccess() {
19+
return success;
20+
}
21+
22+
@Override
23+
public String toString() {
24+
return asString();
25+
}
26+
}

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,10 @@ public WorkflowCleanupResult cleanup(P primary, Context<P> context) {
108108
return result;
109109
}
110110

111-
@Override
112111
public Set<DependentResourceNode> getTopLevelDependentResources() {
113112
return topLevelResources;
114113
}
115114

116-
@Override
117115
public Set<DependentResourceNode> getBottomLevelResource() {
118116
return bottomLevelResource;
119117
}
@@ -140,6 +138,11 @@ public boolean isEmpty() {
140138
return dependentResourceNodes.isEmpty();
141139
}
142140

141+
@Override
142+
public int size() {
143+
return dependentResourceNodes.size();
144+
}
145+
143146
@Override
144147
public Map<String, DependentResource> getDependentResourcesByName() {
145148
final var resources = new HashMap<String, DependentResource>(dependentResourceNodes.size());

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java

+24-20
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ class DependentResourceNode<R, P extends HasMetadata> {
1313
private final List<DependentResourceNode> dependsOn = new LinkedList<>();
1414
private final List<DependentResourceNode> parents = new LinkedList<>();
1515

16-
private Condition<R, P> reconcilePrecondition;
17-
private Condition<R, P> deletePostcondition;
18-
private Condition<R, P> readyPostcondition;
19-
private Condition<R, P> activationCondition;
16+
private ConditionWithType<R, P, ?> reconcilePrecondition;
17+
private ConditionWithType<R, P, ?> deletePostcondition;
18+
private ConditionWithType<R, P, ?> readyPostcondition;
19+
private ConditionWithType<R, P, ?> activationCondition;
2020
private final DependentResource<R, P> dependentResource;
2121

2222
DependentResourceNode(DependentResource<R, P> dependentResource) {
@@ -26,10 +26,10 @@ class DependentResourceNode<R, P extends HasMetadata> {
2626
public DependentResourceNode(Condition<R, P> reconcilePrecondition,
2727
Condition<R, P> deletePostcondition, Condition<R, P> readyPostcondition,
2828
Condition<R, P> activationCondition, DependentResource<R, P> dependentResource) {
29-
this.reconcilePrecondition = reconcilePrecondition;
30-
this.deletePostcondition = deletePostcondition;
31-
this.readyPostcondition = readyPostcondition;
32-
this.activationCondition = activationCondition;
29+
setReconcilePrecondition(reconcilePrecondition);
30+
setDeletePostcondition(deletePostcondition);
31+
setReadyPostcondition(readyPostcondition);
32+
setActivationCondition(activationCondition);
3333
this.dependentResource = dependentResource;
3434
}
3535

@@ -50,36 +50,40 @@ public List<DependentResourceNode> getParents() {
5050
return parents;
5151
}
5252

53-
public Optional<Condition<R, P>> getReconcilePrecondition() {
53+
public Optional<ConditionWithType<R, P, ?>> getReconcilePrecondition() {
5454
return Optional.ofNullable(reconcilePrecondition);
5555
}
5656

57-
public Optional<Condition<R, P>> getDeletePostcondition() {
57+
public Optional<ConditionWithType<R, P, ?>> getDeletePostcondition() {
5858
return Optional.ofNullable(deletePostcondition);
5959
}
6060

61-
public Optional<Condition<R, P>> getActivationCondition() {
61+
public Optional<ConditionWithType<R, P, ?>> getActivationCondition() {
6262
return Optional.ofNullable(activationCondition);
6363
}
6464

65-
void setReconcilePrecondition(Condition<R, P> reconcilePrecondition) {
66-
this.reconcilePrecondition = reconcilePrecondition;
65+
public Optional<ConditionWithType<R, P, ?>> getReadyPostcondition() {
66+
return Optional.ofNullable(readyPostcondition);
6767
}
6868

69-
void setDeletePostcondition(Condition<R, P> cleanupCondition) {
70-
this.deletePostcondition = cleanupCondition;
69+
void setReconcilePrecondition(Condition<R, P> reconcilePrecondition) {
70+
this.reconcilePrecondition = reconcilePrecondition == null ? null
71+
: new ConditionWithType<>(reconcilePrecondition, Condition.Type.RECONCILE);
7172
}
7273

73-
void setActivationCondition(Condition<R, P> activationCondition) {
74-
this.activationCondition = activationCondition;
74+
void setDeletePostcondition(Condition<R, P> deletePostcondition) {
75+
this.deletePostcondition = deletePostcondition == null ? null
76+
: new ConditionWithType<>(deletePostcondition, Condition.Type.DELETE);
7577
}
7678

77-
public Optional<Condition<R, P>> getReadyPostcondition() {
78-
return Optional.ofNullable(readyPostcondition);
79+
void setActivationCondition(Condition<R, P> activationCondition) {
80+
this.activationCondition = activationCondition == null ? null
81+
: new ConditionWithType<>(activationCondition, Condition.Type.ACTIVATION);
7982
}
8083

8184
void setReadyPostcondition(Condition<R, P> readyPostcondition) {
82-
this.readyPostcondition = readyPostcondition;
85+
this.readyPostcondition = readyPostcondition == null ? null
86+
: new ConditionWithType<>(readyPostcondition, Condition.Type.READY);
8387
}
8488

8589
public DependentResource<R, P> getDependentResource() {

0 commit comments

Comments
 (0)