Skip to content

Commit 3670e99

Browse files
csvirimetacosm
authored andcommitted
feat: explicit workflow invocation (#2289)
Signed-off-by: Attila Mészáros <[email protected]>
1 parent 082de83 commit 3670e99

23 files changed

+453
-24
lines changed

Diff for: 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.

Diff for: 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

Diff for: 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
}

Diff for: 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

Diff for: 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

Diff for: 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-3
Original file line numberDiff line numberDiff line change
@@ -3,14 +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 {
11-
public static final Object RECONCILE_RESULT_KEY = new Object();
12-
public static final Object CLEANUP_RESULT_KEY = new Object();
13+
public class DefaultManagedWorkflowAndDependentResourceContext<P extends HasMetadata>
14+
implements ManagedWorkflowAndDependentResourceContext {
15+
16+
public static final Object RECONCILE_RESULT_KEY = new Object();
17+
public static final Object CLEANUP_RESULT_KEY = new Object();
1318
private final ConcurrentHashMap attributes = new ConcurrentHashMap();
19+
private final Controller<P> controller;
20+
private final P primaryResource;
21+
private final Context<P> context;
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+
}
1430

1531
@Override
1632
public <T> Optional<T> get(Object key, Class<T> expectedType) {
@@ -45,4 +61,21 @@ public Optional<WorkflowReconcileResult> getWorkflowReconcileResult() {
4561
public Optional<WorkflowCleanupResult> getWorkflowCleanupResult() {
4662
return get(CLEANUP_RESULT_KEY, WorkflowCleanupResult.class);
4763
}
64+
65+
@Override
66+
public void reconcileManagedWorkflow() {
67+
if (!controller.isWorkflowExplicitInvocation()) {
68+
throw new IllegalStateException("Workflow explicit invocation is not set.");
69+
}
70+
controller.reconcileManagedWorkflow(primaryResource, context);
71+
}
72+
73+
@Override
74+
public void cleanupManageWorkflow() {
75+
if (!controller.isWorkflowExplicitInvocation()) {
76+
throw new IllegalStateException("Workflow explicit invocation is not set.");
77+
}
78+
controller.cleanupManagedWorkflow(primaryResource, context);
79+
}
80+
4881
}

Diff for: operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java renamed to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedWorkflowAndDependentResourceContext.java

+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
}

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

+53-6
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,9 +23,18 @@
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;
@@ -129,9 +143,11 @@ public Map<String, Object> metadata() {
129143
@Override
130144
public UpdateControl<P> execute() throws Exception {
131145
initContextIfNeeded(resource, context);
132-
if (!managedWorkflow.isEmpty()) {
133-
managedWorkflow.reconcile(resource, context);
134-
}
146+
configuration.getWorkflowSpec().ifPresent(ws -> {
147+
if (!managedWorkflow.isEmpty() && !isWorkflowExplicitInvocation()) {
148+
managedWorkflow.reconcile(resource, context);
149+
}
150+
});
135151
return reconciler.reconcile(resource, context);
136152
}
137153
});
@@ -171,9 +187,12 @@ public Map<String, Object> metadata() {
171187
public DeleteControl execute() {
172188
initContextIfNeeded(resource, context);
173189
WorkflowCleanupResult workflowCleanupResult = null;
174-
if (managedWorkflow.hasCleaner()) {
190+
191+
// The cleanup is called also when explicit invocation is true, but the cleaner is not implemented
192+
if (managedWorkflow.hasCleaner() || !isWorkflowExplicitInvocation()) {
175193
workflowCleanupResult = managedWorkflow.cleanup(resource, context);
176194
}
195+
177196
if (isCleaner) {
178197
var cleanupResult = ((Cleaner<P>) reconciler).cleanup(resource, context);
179198
if (!cleanupResult.isRemoveFinalizer()) {
@@ -430,4 +449,32 @@ public ExecutorServiceManager getExecutorServiceManager() {
430449
public EventSourceContext<P> eventSourceContext() {
431450
return eventSourceContext;
432451
}
452+
453+
public void reconcileManagedWorkflow(P primary, Context<P> context) {
454+
if (!managedWorkflow.isEmpty()) {
455+
var res = managedWorkflow.reconcile(primary, context);
456+
((DefaultManagedWorkflowAndDependentResourceContext) context
457+
.managedWorkflowAndDependentResourceContext())
458+
.setWorkflowExecutionResult(res);
459+
res.throwAggregateExceptionIfErrorsPresent();
460+
}
461+
}
462+
463+
public WorkflowCleanupResult cleanupManagedWorkflow(P resource, Context<P> context) {
464+
if (managedWorkflow.hasCleaner()) {
465+
var workflowCleanupResult = managedWorkflow.cleanup(resource, context);
466+
((DefaultManagedWorkflowAndDependentResourceContext) context
467+
.managedWorkflowAndDependentResourceContext())
468+
.setWorkflowCleanupResult(workflowCleanupResult);
469+
workflowCleanupResult.throwAggregateExceptionIfErrorsPresent();
470+
return workflowCleanupResult;
471+
} else {
472+
return null;
473+
}
474+
}
475+
476+
public boolean isWorkflowExplicitInvocation() {
477+
return configuration.getWorkflowSpec().map(WorkflowSpec::isExplicitInvocation)
478+
.orElse(false);
479+
}
433480
}

Diff for: 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()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.javaoperatorsdk.operator;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.extension.RegisterExtension;
5+
6+
import io.fabric8.kubernetes.api.model.ConfigMap;
7+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
8+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
9+
import io.javaoperatorsdk.operator.sample.workflowexplicitcleanup.WorkflowExplicitCleanupCustomResource;
10+
import io.javaoperatorsdk.operator.sample.workflowexplicitcleanup.WorkflowExplicitCleanupReconciler;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
import static org.awaitility.Awaitility.await;
14+
15+
public class WorkflowExplicitCleanupIT {
16+
17+
public static final String RESOURCE_NAME = "test1";
18+
19+
@RegisterExtension
20+
LocallyRunOperatorExtension extension =
21+
LocallyRunOperatorExtension.builder()
22+
.withReconciler(WorkflowExplicitCleanupReconciler.class)
23+
.build();
24+
25+
@Test
26+
void workflowInvokedExplicitly() {
27+
var res = extension.create(testResource());
28+
29+
await().untilAsserted(() -> {
30+
assertThat(extension.get(ConfigMap.class, RESOURCE_NAME)).isNotNull();
31+
});
32+
33+
extension.delete(res);
34+
35+
// The ConfigMap is not garbage collected, this tests that even if the cleaner is not
36+
// implemented the workflow cleanup still called even if there is explicit invocation
37+
await().untilAsserted(() -> {
38+
assertThat(extension.get(ConfigMap.class, RESOURCE_NAME)).isNull();
39+
});
40+
}
41+
42+
WorkflowExplicitCleanupCustomResource testResource() {
43+
var res = new WorkflowExplicitCleanupCustomResource();
44+
res.setMetadata(new ObjectMetaBuilder()
45+
.withName(RESOURCE_NAME)
46+
.build());
47+
return res;
48+
}
49+
50+
}

0 commit comments

Comments
 (0)