Skip to content

Commit dcbf7d6

Browse files
committed
feat: activation condition (operator-framework#2105)
Signed-off-by: Attila Mészáros <csviri@gmail.com>
1 parent 7db1503 commit dcbf7d6

File tree

39 files changed

+961
-122
lines changed

39 files changed

+961
-122
lines changed

docs/documentation/workflows.md

+20-8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ reconciliation process.
3535
proceeding until the condition checking whether the DR is ready holds true
3636
- **Delete postcondition** - is a condition on a given DR to check if the reconciliation of
3737
dependents can proceed after the DR is supposed to have been deleted
38+
- **Activation condition** - is a special condition meant to specify under which condition the DR is used in the
39+
workflow. A typical use-case for this feature is to only activate some dependents depending on the presence of
40+
optional resources / features on the target cluster. Without this activation condition, JOSDK would attempt to
41+
register an informer for these optional resources, which would cause an error in the case where the resource is
42+
missing. With this activation condition, you can now conditionally register informers depending on whether the
43+
condition holds or not. This is a very useful feature when your operator needs to handle different flavors of the
44+
platform (e.g. OpenShift vs plain Kubernetes) and/or change its behavior based on the availability of optional
45+
resources / features (e.g. CertManager, a specific Ingress controller, etc.).
3846

3947
## Defining Workflows
4048

@@ -66,6 +74,7 @@ will only consider the `ConfigMap` deleted until that post-condition becomes `tr
6674
@Dependent(type = ConfigMapDependentResource.class,
6775
reconcilePrecondition = ConfigMapReconcileCondition.class,
6876
deletePostcondition = ConfigMapDeletePostCondition.class,
77+
activationCondition = ConfigMapActivationCondition.class,
6978
dependsOn = DEPLOYMENT_NAME)
7079
})
7180
public class SampleWorkflowReconciler implements Reconciler<WorkflowAllFeatureCustomResource>,
@@ -165,7 +174,7 @@ executed if a resource is marked for deletion.
165174
## Common Principles
166175

167176
- **As complete as possible execution** - when a workflow is reconciled, it tries to reconcile as
168-
many resources as possible. Thus if an error happens or a ready condition is not met for a
177+
many resources as possible. Thus, if an error happens or a ready condition is not met for a
169178
resources, all the other independent resources will be still reconciled. This is the opposite
170179
to a fail-fast approach. The assumption is that eventually in this way the overall state will
171180
converge faster towards the desired state than would be the case if the reconciliation was
@@ -186,13 +195,13 @@ demonstrated using examples:
186195
`depends-on` relations.
187196
2. Root nodes, i.e. nodes in the graph that do not depend on other nodes are reconciled first,
188197
in a parallel manner.
189-
2. A DR is reconciled if it does not depend on any other DRs, or *ALL* the DRs it depends on are
190-
reconciled and ready. If a DR defines a reconcile pre-condition, then this condition must
191-
become `true` before the DR is reconciled.
192-
2. A DR is considered *ready* if it got successfully reconciled and any ready post-condition it
198+
3. A DR is reconciled if it does not depend on any other DRs, or *ALL* the DRs it depends on are
199+
reconciled and ready. If a DR defines a reconcile pre-condition and/or an activation condition,
200+
then these condition must become `true` before the DR is reconciled.
201+
4. A DR is considered *ready* if it got successfully reconciled and any ready post-condition it
193202
might define is `true`.
194-
3. If a DR's reconcile pre-condition is not met, this DR is deleted. All of the DRs that depend
195-
on the dependent resource being considered are also recursively deleted. This implies that
203+
5. If a DR's reconcile pre-condition is not met, this DR is deleted. All the DRs that depend
204+
on the dependent resource are also recursively deleted. This implies that
196205
DRs are deleted in reverse order compared the one in which they are reconciled. The reason
197206
for this behavior is (Will make a more detailed blog post about the design decision, much deeper
198207
than the reference documentation)
@@ -202,7 +211,10 @@ demonstrated using examples:
202211
idempotency (i.e. with the same input state, we should have the same output state), from this
203212
follows that if the condition doesn't hold `true` anymore, the associated resource needs to
204213
be deleted because the resource shouldn't exist/have been created.
205-
4. For a DR to be deleted by a workflow, it needs to implement the `Deleter` interface, in which
214+
6. If a DR's activation condition is not met, it won't be reconciled or deleted. If other DR's depend on it, those will
215+
be recursively deleted in a way similar to reconcile pre-conditions. Event sources for a dependent resource with
216+
activation condition are registered/de-registered dynamically, thus during the reconciliation.
217+
7. For a DR to be deleted by a workflow, it needs to implement the `Deleter` interface, in which
206218
case its `delete` method will be called, unless it also implements the `GarbageCollected`
207219
interface. If a DR doesn't implement `Deleter` it is considered as automatically deleted. If
208220
a delete post-condition exists for this DR, it needs to become `true` for the workflow to

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java

+1
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ private static List<DependentResourceSpec> dependentResources(
230230
Utils.instantiate(dependent.readyPostcondition(), Condition.class, context),
231231
Utils.instantiate(dependent.reconcilePrecondition(), Condition.class, context),
232232
Utils.instantiate(dependent.deletePostcondition(), Condition.class, context),
233+
Utils.instantiate(dependent.activationCondition(), Condition.class, context),
233234
eventSourceName);
234235
specsMap.put(dependentName, spec);
235236
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,21 @@ public class DependentResourceSpec<R, P extends HasMetadata> {
2222

2323
private final Condition<?, ?> deletePostCondition;
2424

25+
private final Condition<?, ?> activationCondition;
26+
2527
private final String useEventSourceWithName;
2628

2729
public DependentResourceSpec(Class<? extends DependentResource<R, P>> dependentResourceClass,
2830
String name, Set<String> dependsOn, Condition<?, ?> readyCondition,
2931
Condition<?, ?> reconcileCondition, Condition<?, ?> deletePostCondition,
30-
String useEventSourceWithName) {
32+
Condition<?, ?> activationCondition, String useEventSourceWithName) {
3133
this.dependentResourceClass = dependentResourceClass;
3234
this.name = name;
3335
this.dependsOn = dependsOn;
3436
this.readyCondition = readyCondition;
3537
this.reconcileCondition = reconcileCondition;
3638
this.deletePostCondition = deletePostCondition;
39+
this.activationCondition = activationCondition;
3740
this.useEventSourceWithName = useEventSourceWithName;
3841
}
3942

@@ -87,6 +90,11 @@ public Condition getDeletePostCondition() {
8790
return deletePostCondition;
8891
}
8992

93+
@SuppressWarnings("rawtypes")
94+
public Condition getActivationCondition() {
95+
return activationCondition;
96+
}
97+
9098
public Optional<String> getUseEventSourceWithName() {
9199
return Optional.ofNullable(useEventSourceWithName);
92100
}

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java

+20
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@
5050
*/
5151
Class<? extends Condition> deletePostcondition() default Condition.class;
5252

53+
/**
54+
* <p>
55+
* A condition that needs to become true for the dependent to even be considered as part of the
56+
* workflow. This is useful for dependents that represent optional resources on the cluster and
57+
* might not be present. In this case, a reconcile pre-condition is not enough because in that
58+
* situation, the associated informer will still be registered. With this activation condition,
59+
* the associated event source will only be registered if the condition is met. Otherwise, this
60+
* behaves like a reconcile pre-condition in the sense that dependents, that depend on this one,
61+
* will only get created if the condition is met and will get deleted if the condition becomes
62+
* false.
63+
* </p>
64+
* <p>
65+
* As other conditions, this gets evaluated at the beginning of every reconciliation, which means
66+
* that it allows to react to optional resources becoming available on the cluster as the operator
67+
* runs. More specifically, this means that the associated event source can get dynamically
68+
* registered or de-registered during reconciliation.
69+
* </p>
70+
*/
71+
Class<? extends Condition> activationCondition() default Condition.class;
72+
5373
/**
5474
* The list of named dependents that need to be reconciled before this one can be.
5575
*

operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public class Controller<P extends HasMetadata>
7676
private final GroupVersionKind associatedGVK;
7777
private final EventProcessor<P> eventProcessor;
7878
private final ControllerHealthInfo controllerHealthInfo;
79+
private final EventSourceContext<P> eventSourceContext;
7980

8081
public Controller(Reconciler<P> reconciler,
8182
ControllerConfiguration<P> configuration,
@@ -98,9 +99,9 @@ public Controller(Reconciler<P> reconciler,
9899
eventProcessor = new EventProcessor<>(eventSourceManager, configurationService);
99100
eventSourceManager.postProcessDefaultEventSourcesAfterProcessorInitializer();
100101
controllerHealthInfo = new ControllerHealthInfo(eventSourceManager);
101-
final var context = new EventSourceContext<>(
102+
eventSourceContext = new EventSourceContext<>(
102103
eventSourceManager.getControllerResourceEventSource(), configuration, kubernetesClient);
103-
initAndRegisterEventSources(context);
104+
initAndRegisterEventSources(eventSourceContext);
104105
configurationService.getMetrics().controllerRegistered(this);
105106
}
106107

@@ -236,7 +237,8 @@ public void initAndRegisterEventSources(EventSourceContext<P> context) {
236237
}
237238

238239
// register created event sources
239-
final var dependentResourcesByName = managedWorkflow.getDependentResourcesByName();
240+
final var dependentResourcesByName =
241+
managedWorkflow.getDependentResourcesByNameWithoutActivationCondition();
240242
final var size = dependentResourcesByName.size();
241243
if (size > 0) {
242244
dependentResourcesByName.forEach((key, dependentResource) -> {
@@ -440,4 +442,8 @@ public EventProcessor<P> getEventProcessor() {
440442
public ExecutorServiceManager getExecutorServiceManager() {
441443
return getConfiguration().getConfigurationService().getExecutorServiceManager();
442444
}
445+
446+
public EventSourceContext<P> eventSourceContext() {
447+
return eventSourceContext;
448+
}
443449
}

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,15 @@ public AbstractWorkflowExecutor(Workflow<P> workflow, P primary, Context<P> cont
4646
protected synchronized void waitForScheduledExecutionsToRun() {
4747
while (true) {
4848
try {
49-
this.wait();
49+
// in case when workflow just contains non-activated dependents,
50+
// it needs to be checked first if there are already no executions
51+
// scheduled at the beginning.
5052
if (noMoreExecutionsScheduled()) {
5153
break;
5254
} else {
5355
logger().warn("Notified but still resources under execution. This should not happen.");
5456
}
57+
this.wait();
5558
} catch (InterruptedException e) {
5659
if (noMoreExecutionsScheduled()) {
5760
logger().debug("interrupted, no more executions for: {}", primaryID);

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

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ public Workflow<P> resolve(KubernetesClient client,
8181
spec.getReconcileCondition(),
8282
spec.getDeletePostCondition(),
8383
spec.getReadyCondition(),
84+
spec.getActivationCondition(),
8485
resolve(spec, client, configuration));
8586
alreadyResolved.put(node.getName(), node);
8687
spec.getDependsOn()

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

+11
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,15 @@ public Map<String, DependentResource> getDependentResourcesByName() {
147147
.forEach((name, node) -> resources.put(name, node.getDependentResource()));
148148
return resources;
149149
}
150+
151+
public Map<String, DependentResource> getDependentResourcesByNameWithoutActivationCondition() {
152+
final var resources = new HashMap<String, DependentResource>(dependentResourceNodes.size());
153+
dependentResourceNodes
154+
.forEach((name, node) -> {
155+
if (node.getActivationCondition().isEmpty()) {
156+
resources.put(name, node.getDependentResource());
157+
}
158+
});
159+
return resources;
160+
}
150161
}

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

+12-2
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,21 @@ public class DependentResourceNode<R, P extends HasMetadata> {
1616
private Condition<R, P> reconcilePrecondition;
1717
private Condition<R, P> deletePostcondition;
1818
private Condition<R, P> readyPostcondition;
19+
private Condition<R, P> activationCondition;
1920
private final DependentResource<R, P> dependentResource;
2021

2122
DependentResourceNode(DependentResource<R, P> dependentResource) {
22-
this(getNameFor(dependentResource), null, null, null, dependentResource);
23+
this(getNameFor(dependentResource), null, null, null, null, dependentResource);
2324
}
2425

2526
public DependentResourceNode(String name, Condition<R, P> reconcilePrecondition,
2627
Condition<R, P> deletePostcondition, Condition<R, P> readyPostcondition,
27-
DependentResource<R, P> dependentResource) {
28+
Condition<R, P> activationCondition, DependentResource<R, P> dependentResource) {
2829
this.name = name;
2930
this.reconcilePrecondition = reconcilePrecondition;
3031
this.deletePostcondition = deletePostcondition;
3132
this.readyPostcondition = readyPostcondition;
33+
this.activationCondition = activationCondition;
3234
this.dependentResource = dependentResource;
3335
}
3436

@@ -63,6 +65,10 @@ public Optional<Condition<R, P>> getDeletePostcondition() {
6365
return Optional.ofNullable(deletePostcondition);
6466
}
6567

68+
public Optional<Condition<R, P>> getActivationCondition() {
69+
return Optional.ofNullable(activationCondition);
70+
}
71+
6672
void setReconcilePrecondition(Condition<R, P> reconcilePrecondition) {
6773
this.reconcilePrecondition = reconcilePrecondition;
6874
}
@@ -71,6 +77,10 @@ void setDeletePostcondition(Condition<R, P> cleanupCondition) {
7177
this.deletePostcondition = cleanupCondition;
7278
}
7379

80+
void setActivationCondition(Condition<R, P> activationCondition) {
81+
this.activationCondition = activationCondition;
82+
}
83+
7484
public Optional<Condition<R, P>> getReadyPostcondition() {
7585
return Optional.ofNullable(readyPostcondition);
7686
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,9 @@ default boolean isEmpty() {
4242
default Map<String, DependentResource> getDependentResourcesByName() {
4343
return Collections.emptyMap();
4444
}
45+
46+
@SuppressWarnings("rawtypes")
47+
default Map<String, DependentResource> getDependentResourcesByNameWithoutActivationCondition() {
48+
return Collections.emptyMap();
49+
}
4550
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ public WorkflowBuilder<P> withDeletePostcondition(Condition deletePostcondition)
5858
return this;
5959
}
6060

61+
public WorkflowBuilder<P> withActivationCondition(Condition activationCondition) {
62+
currentNode.setActivationCondition(activationCondition);
63+
return this;
64+
}
65+
6166
DependentResourceNode getNodeByDependentResource(DependentResource<?, ?> dependentResource) {
6267
// first check by name
6368
final var node =

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

+11-2
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,20 @@ protected void doRun(DependentResourceNode<R, P> dependentResourceNode,
6868
DependentResource<R, P> dependentResource) {
6969
var deletePostCondition = dependentResourceNode.getDeletePostcondition();
7070

71-
if (dependentResource.isDeletable()) {
71+
var active =
72+
isConditionMet(dependentResourceNode.getActivationCondition(), dependentResource);
73+
74+
if (dependentResource.isDeletable() && active) {
7275
((Deleter<P>) dependentResource).delete(primary, context);
7376
deleteCalled.add(dependentResourceNode);
7477
}
75-
boolean deletePostConditionMet = isConditionMet(deletePostCondition, dependentResource);
78+
79+
boolean deletePostConditionMet;
80+
if (active) {
81+
deletePostConditionMet = isConditionMet(deletePostCondition, dependentResource);
82+
} else {
83+
deletePostConditionMet = true;
84+
}
7685
if (deletePostConditionMet) {
7786
markAsVisited(dependentResourceNode);
7887
handleDependentCleaned(dependentResourceNode);

0 commit comments

Comments
 (0)