Skip to content

Commit 986d20a

Browse files
authored
feat: explicit workflow invocation (#2289)
Signed-off-by: Attila Mészáros <[email protected]>
1 parent b720b16 commit 986d20a

24 files changed

+484
-33
lines changed

docs/documentation/v5-0-migration.md

+2
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ permalink: /docs/v5-0-migration
1717
[`EventSourceUtils`](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/EventSourceUtils.java#L11-L11)
1818
now contains all the utility methods used for event sources naming that were previously defined in
1919
the `EventSourceInitializer` interface.
20+
3. `ManagedDependentResourceContext` has been renamed to `ManagedWorkflowAndDependentResourceContext` and is accessed
21+
via the accordingly renamed `managedWorkflowAndDependentResourceContext` method.

docs/documentation/workflows.md

+28
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,34 @@ and NOT `CRUDKubernetesDependentResource` since otherwise the Kubernetes Garbage
338338
In other words if a Kubernetes Dependent Resource depends on another dependent resource, it should not implement
339339
`GargageCollected` interface, otherwise the deletion order won't be guaranteed.
340340

341+
342+
## Explicit Managed Workflow Invocation
343+
344+
Managed workflows, i.e. ones that are declared via annotations and therefore completely managed by JOSDK, are reconciled
345+
before the primary resource. Each dependent resource that can be reconciled (according to the workflow configuration)
346+
will therefore be reconciled before the primary reconciler is called to reconcile the primary resource. There are,
347+
however, situations where it would be be useful to perform additional steps before the workflow is reconciled, for
348+
example to validate the current state, execute arbitrary logic or even skip reconciliation altogether. Explicit
349+
invocation of managed workflow was therefore introduced to solve these issues.
350+
351+
To use this feature, you need to set the `explicitInvocation` field to `true` on the `@Workflow` annotation and then
352+
call the `reconcileManagedWorkflow` method from the `
353+
ManagedWorkflowAndDependentResourceContext` retrieved from the reconciliation `Context` provided as part of your primary
354+
resource reconciler `reconcile` method arguments.
355+
356+
See
357+
related [integration test](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowExplicitInvocationIT.java)
358+
for more details.
359+
360+
For `cleanup`, if the `Cleaner` interface is implemented, the `cleanupManageWorkflow()` needs to be called explicitly.
361+
However, if `Cleaner` interface is not implemented, it will be called implicitly.
362+
See
363+
related [integration test](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowExplicitCleanupIT.java).
364+
365+
While nothing prevents calling the workflow multiple times in a reconciler, it isn't typical or even recommended to do
366+
so. Conversely, if explicit invocation is requested but `reconcileManagedWorkflow` is not called in the primary resource
367+
reconciler, the workflow won't be reconciled at all.
368+
341369
## Notes and Caveats
342370

343371
- Delete is almost always called on every resource during the cleanup. However, it might be the case

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ protected <P extends HasMetadata> ControllerConfiguration<P> configFor(Reconcile
169169
io.javaoperatorsdk.operator.api.reconciler.Workflow.class);
170170
if (workflowAnnotation != null) {
171171
List<DependentResourceSpec> specs = dependentResources(workflowAnnotation, config);
172-
WorkflowSpec workflowSpec = new WorkflowSpec(specs);
172+
WorkflowSpec workflowSpec = new WorkflowSpec(specs, workflowAnnotation.explicitInvocation());
173173
config.setWorkflowSpec(workflowSpec);
174174
}
175175

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

+8-1
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,19 @@ public class WorkflowSpec {
88

99
@SuppressWarnings("rawtypes")
1010
private final List<DependentResourceSpec> dependentResourceSpecs;
11+
private final boolean explicitInvocation;
1112

12-
public WorkflowSpec(List<DependentResourceSpec> dependentResourceSpecs) {
13+
public WorkflowSpec(List<DependentResourceSpec> dependentResourceSpecs,
14+
boolean explicitInvocation) {
1315
this.dependentResourceSpecs = dependentResourceSpecs;
16+
this.explicitInvocation = explicitInvocation;
1417
}
1518

1619
public List<DependentResourceSpec> getDependentResourceSpecs() {
1720
return dependentResourceSpecs;
1821
}
22+
23+
public boolean isExplicitInvocation() {
24+
return explicitInvocation;
25+
}
1926
}

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

+9-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import io.fabric8.kubernetes.api.model.HasMetadata;
99
import io.fabric8.kubernetes.client.KubernetesClient;
1010
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
11-
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceContext;
11+
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext;
1212
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
1313
import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache;
1414

@@ -34,7 +34,14 @@ <R> Optional<R> getSecondaryResource(Class<R> expectedType,
3434

3535
ControllerConfiguration<P> getControllerConfiguration();
3636

37-
ManagedDependentResourceContext managedDependentResourceContext();
37+
/**
38+
* Retrieve the {@link ManagedWorkflowAndDependentResourceContext} used to interact with
39+
* {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource}s and associated
40+
* {@link io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow}
41+
*
42+
* @return the {@link ManagedWorkflowAndDependentResourceContext}
43+
*/
44+
ManagedWorkflowAndDependentResourceContext managedWorkflowAndDependentResourceContext();
3845

3946
EventSourceRetriever<P> eventSourceRetriever();
4047

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

+6-5
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import io.fabric8.kubernetes.api.model.HasMetadata;
1010
import io.fabric8.kubernetes.client.KubernetesClient;
1111
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
12-
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext;
13-
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceContext;
12+
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedWorkflowAndDependentResourceContext;
13+
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext;
1414
import io.javaoperatorsdk.operator.processing.Controller;
1515
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
1616
import io.javaoperatorsdk.operator.processing.event.ResourceID;
@@ -21,14 +21,15 @@ public class DefaultContext<P extends HasMetadata> implements Context<P> {
2121
private final Controller<P> controller;
2222
private final P primaryResource;
2323
private final ControllerConfiguration<P> controllerConfiguration;
24-
private final DefaultManagedDependentResourceContext defaultManagedDependentResourceContext;
24+
private final DefaultManagedWorkflowAndDependentResourceContext<P> defaultManagedDependentResourceContext;
2525

2626
public DefaultContext(RetryInfo retryInfo, Controller<P> controller, P primaryResource) {
2727
this.retryInfo = retryInfo;
2828
this.controller = controller;
2929
this.primaryResource = primaryResource;
3030
this.controllerConfiguration = controller.getConfiguration();
31-
this.defaultManagedDependentResourceContext = new DefaultManagedDependentResourceContext();
31+
this.defaultManagedDependentResourceContext =
32+
new DefaultManagedWorkflowAndDependentResourceContext<>(controller, primaryResource, this);
3233
}
3334

3435
@Override
@@ -79,7 +80,7 @@ public ControllerConfiguration<P> getControllerConfiguration() {
7980
}
8081

8182
@Override
82-
public ManagedDependentResourceContext managedDependentResourceContext() {
83+
public ManagedWorkflowAndDependentResourceContext managedWorkflowAndDependentResourceContext() {
8384
return defaultManagedDependentResourceContext;
8485
}
8586

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

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

33
import java.lang.annotation.*;
44

5+
import io.fabric8.kubernetes.api.model.HasMetadata;
56
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
67

78
@Inherited
@@ -11,4 +12,11 @@
1112

1213
Dependent[] dependents();
1314

15+
/**
16+
* If true, managed workflow should be explicitly invoked within the reconciler implementation. If
17+
* false workflow is invoked just before the {@link Reconciler#reconcile(HasMetadata, Context)}
18+
* method.
19+
*/
20+
boolean explicitInvocation() default false;
21+
1422
}
+36-4
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,30 @@
33
import java.util.Optional;
44
import java.util.concurrent.ConcurrentHashMap;
55

6+
import io.fabric8.kubernetes.api.model.HasMetadata;
7+
import io.javaoperatorsdk.operator.api.reconciler.Context;
8+
import io.javaoperatorsdk.operator.processing.Controller;
69
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult;
710
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowReconcileResult;
811

912
@SuppressWarnings("rawtypes")
10-
public class DefaultManagedDependentResourceContext implements ManagedDependentResourceContext {
13+
public class DefaultManagedWorkflowAndDependentResourceContext<P extends HasMetadata>
14+
implements ManagedWorkflowAndDependentResourceContext {
1115

16+
private final ConcurrentHashMap attributes = new ConcurrentHashMap();
17+
private final Controller<P> controller;
18+
private final P primaryResource;
19+
private final Context<P> context;
1220
private WorkflowReconcileResult workflowReconcileResult;
1321
private WorkflowCleanupResult workflowCleanupResult;
14-
private final ConcurrentHashMap attributes = new ConcurrentHashMap();
22+
23+
public DefaultManagedWorkflowAndDependentResourceContext(Controller<P> controller,
24+
P primaryResource,
25+
Context<P> context) {
26+
this.controller = controller;
27+
this.primaryResource = primaryResource;
28+
this.context = context;
29+
}
1530

1631
@Override
1732
public <T> Optional<T> get(Object key, Class<T> expectedType) {
@@ -37,13 +52,13 @@ public <T> T getMandatory(Object key, Class<T> expectedType) {
3752
+ ") is missing or not of the expected type"));
3853
}
3954

40-
public DefaultManagedDependentResourceContext setWorkflowExecutionResult(
55+
public DefaultManagedWorkflowAndDependentResourceContext setWorkflowExecutionResult(
4156
WorkflowReconcileResult workflowReconcileResult) {
4257
this.workflowReconcileResult = workflowReconcileResult;
4358
return this;
4459
}
4560

46-
public DefaultManagedDependentResourceContext setWorkflowCleanupResult(
61+
public DefaultManagedWorkflowAndDependentResourceContext setWorkflowCleanupResult(
4762
WorkflowCleanupResult workflowCleanupResult) {
4863
this.workflowCleanupResult = workflowCleanupResult;
4964
return this;
@@ -58,4 +73,21 @@ public WorkflowReconcileResult getWorkflowReconcileResult() {
5873
public WorkflowCleanupResult getWorkflowCleanupResult() {
5974
return workflowCleanupResult;
6075
}
76+
77+
@Override
78+
public void reconcileManagedWorkflow() {
79+
if (!controller.isWorkflowExplicitInvocation()) {
80+
throw new IllegalStateException("Workflow explicit invocation is not set.");
81+
}
82+
controller.reconcileManagedWorkflow(primaryResource, context);
83+
}
84+
85+
@Override
86+
public void cleanupManageWorkflow() {
87+
if (!controller.isWorkflowExplicitInvocation()) {
88+
throw new IllegalStateException("Workflow explicit invocation is not set.");
89+
}
90+
controller.cleanupManagedWorkflow(primaryResource, context);
91+
}
92+
6193
}
+21-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* Contextual information related to {@link DependentResource} either to retrieve the actual
1111
* implementations to interact with them or to pass information between them and/or the reconciler
1212
*/
13-
public interface ManagedDependentResourceContext {
13+
public interface ManagedWorkflowAndDependentResourceContext {
1414

1515
/**
1616
* Retrieve a contextual object, if it exists and is of the specified expected type, associated
@@ -37,7 +37,6 @@ public interface ManagedDependentResourceContext {
3737
* @return an Optional containing the previous value associated with the key or
3838
* {@link Optional#empty()} if none existed
3939
*/
40-
@SuppressWarnings("unchecked")
4140
<T> T put(Object key, T value);
4241

4342
/**
@@ -54,5 +53,25 @@ public interface ManagedDependentResourceContext {
5453

5554
WorkflowReconcileResult getWorkflowReconcileResult();
5655

56+
@SuppressWarnings("unused")
5757
WorkflowCleanupResult getWorkflowCleanupResult();
58+
59+
/**
60+
* Explicitly reconcile the declared workflow for the associated
61+
* {@link io.javaoperatorsdk.operator.api.reconciler.Reconciler}
62+
*
63+
* @throws IllegalStateException if called when explicit invocation is not requested
64+
*/
65+
void reconcileManagedWorkflow();
66+
67+
/**
68+
* Explicitly clean-up dependent resources in the declared workflow for the associated
69+
* {@link io.javaoperatorsdk.operator.api.reconciler.Reconciler}. Note that calling this method is
70+
* only needed if the associated reconciler implements the
71+
* {@link io.javaoperatorsdk.operator.api.reconciler.Cleaner} interface.
72+
*
73+
* @throws IllegalStateException if called when explicit invocation is not requested
74+
*/
75+
void cleanupManageWorkflow();
76+
5877
}

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

+56-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package io.javaoperatorsdk.operator.processing;
22

3-
import java.util.*;
3+
import java.util.ArrayList;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Optional;
8+
import java.util.Set;
49

510
import org.slf4j.Logger;
611
import org.slf4j.LoggerFactory;
@@ -18,13 +23,22 @@
1823
import io.javaoperatorsdk.operator.RegisteredController;
1924
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
2025
import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager;
26+
import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec;
2127
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
2228
import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution;
23-
import io.javaoperatorsdk.operator.api.reconciler.*;
29+
import io.javaoperatorsdk.operator.api.reconciler.Cleaner;
30+
import io.javaoperatorsdk.operator.api.reconciler.Constants;
31+
import io.javaoperatorsdk.operator.api.reconciler.Context;
32+
import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer;
33+
import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
34+
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
35+
import io.javaoperatorsdk.operator.api.reconciler.Ignore;
36+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
37+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
2438
import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceNotFoundException;
2539
import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider;
2640
import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer;
27-
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext;
41+
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedWorkflowAndDependentResourceContext;
2842
import io.javaoperatorsdk.operator.health.ControllerHealthInfo;
2943
import io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow;
3044
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult;
@@ -130,12 +144,11 @@ public Map<String, Object> metadata() {
130144
@Override
131145
public UpdateControl<P> execute() throws Exception {
132146
initContextIfNeeded(resource, context);
133-
if (!managedWorkflow.isEmpty()) {
134-
var res = managedWorkflow.reconcile(resource, context);
135-
((DefaultManagedDependentResourceContext) context.managedDependentResourceContext())
136-
.setWorkflowExecutionResult(res);
137-
res.throwAggregateExceptionIfErrorsPresent();
138-
}
147+
configuration.getWorkflowSpec().ifPresent(ws -> {
148+
if (!isWorkflowExplicitInvocation()) {
149+
reconcileManagedWorkflow(resource, context);
150+
}
151+
});
139152
return reconciler.reconcile(resource, context);
140153
}
141154
});
@@ -175,12 +188,13 @@ public Map<String, Object> metadata() {
175188
public DeleteControl execute() {
176189
initContextIfNeeded(resource, context);
177190
WorkflowCleanupResult workflowCleanupResult = null;
178-
if (managedWorkflow.hasCleaner()) {
179-
workflowCleanupResult = managedWorkflow.cleanup(resource, context);
180-
((DefaultManagedDependentResourceContext) context.managedDependentResourceContext())
181-
.setWorkflowCleanupResult(workflowCleanupResult);
182-
workflowCleanupResult.throwAggregateExceptionIfErrorsPresent();
191+
192+
// The cleanup is called also when explicit invocation is true, but the cleaner is not
193+
// implemented
194+
if (!isCleaner || !isWorkflowExplicitInvocation()) {
195+
workflowCleanupResult = cleanupManagedWorkflow(resource, context);
183196
}
197+
184198
if (isCleaner) {
185199
var cleanupResult = ((Cleaner<P>) reconciler).cleanup(resource, context);
186200
if (!cleanupResult.isRemoveFinalizer()) {
@@ -429,4 +443,32 @@ public ExecutorServiceManager getExecutorServiceManager() {
429443
public EventSourceContext<P> eventSourceContext() {
430444
return eventSourceContext;
431445
}
446+
447+
public void reconcileManagedWorkflow(P primary, Context<P> context) {
448+
if (!managedWorkflow.isEmpty()) {
449+
var res = managedWorkflow.reconcile(primary, context);
450+
((DefaultManagedWorkflowAndDependentResourceContext) context
451+
.managedWorkflowAndDependentResourceContext())
452+
.setWorkflowExecutionResult(res);
453+
res.throwAggregateExceptionIfErrorsPresent();
454+
}
455+
}
456+
457+
public WorkflowCleanupResult cleanupManagedWorkflow(P resource, Context<P> context) {
458+
if (managedWorkflow.hasCleaner()) {
459+
var workflowCleanupResult = managedWorkflow.cleanup(resource, context);
460+
((DefaultManagedWorkflowAndDependentResourceContext) context
461+
.managedWorkflowAndDependentResourceContext())
462+
.setWorkflowCleanupResult(workflowCleanupResult);
463+
workflowCleanupResult.throwAggregateExceptionIfErrorsPresent();
464+
return workflowCleanupResult;
465+
} else {
466+
return null;
467+
}
468+
}
469+
470+
public boolean isWorkflowExplicitInvocation() {
471+
return configuration.getWorkflowSpec().map(WorkflowSpec::isExplicitInvocation)
472+
.orElse(false);
473+
}
432474
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ ManagedWorkflow managedWorkflow(DependentResourceSpec... specs) {
6464
final var configuration = mock(ControllerConfiguration.class);
6565
final var specList = List.of(specs);
6666

67-
var ws = new WorkflowSpec(specList);
67+
var ws = new WorkflowSpec(specList, false);
6868
when(configuration.getWorkflowSpec()).thenReturn(Optional.of(ws));
6969

7070
return new BaseConfigurationService().getWorkflowFactory()

0 commit comments

Comments
 (0)