Skip to content

Commit 12f5d63

Browse files
committed
IT, docs, impl
Signed-off-by: Attila Mészáros <[email protected]>
1 parent 0112e6f commit 12f5d63

20 files changed

+403
-30
lines changed

Diff for: docs/documentation/workflows.md

+34
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,40 @@ 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 workflow is execution before the `reconcile(Resource,Context)` is called. There are certain situations however
345+
when before the invocation there is additional validation or other logic needs to be executed or in some cases
346+
event the workflow execution skipped. For this reason managed workflow explicit invocation is possible.
347+
348+
```java
349+
350+
@Workflow(explicitInvocation = true,
351+
dependents = @Dependent(type = ConfigMapDependent.class))
352+
@ControllerConfiguration
353+
public class WorkflowExplicitInvocationReconciler
354+
implements Reconciler<WorkflowExplicitInvocationCustomResource> {
355+
356+
@Override
357+
public UpdateControl<WorkflowExplicitInvocationCustomResource> reconcile(
358+
WorkflowExplicitInvocationCustomResource resource,
359+
Context<WorkflowExplicitInvocationCustomResource> context) {
360+
361+
// additional logic before the workflow is executed
362+
363+
context.managedWorkflowAndDependentResourceContext().reconcileManagedWorkflow();
364+
365+
return UpdateControl.noUpdate();
366+
}
367+
368+
```
369+
370+
For `cleanup`, if the `Cleaner` interface is implemented, the `cleanupManageWorkflow()` needs to be called explicitly.
371+
However, if `Cleaner` interface is not implemented, it will be called implicitly.
372+
373+
Nothing prevents calling the workflow multiple times in a reconciler, however it is meant to be called at most once.
374+
341375
## Notes and Caveats
342376

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

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

+2-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,7 @@ <R> Optional<R> getSecondaryResource(Class<R> expectedType,
3434

3535
ControllerConfiguration<P> getControllerConfiguration();
3636

37-
ManagedDependentResourceContext managedDependentResourceContext();
37+
ManagedWorkflowAndDependentResourceContext managedWorkflowAndDependentResourceContext();
3838

3939
EventSourceRetriever<P> eventSourceRetriever();
4040

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

+5-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,15 +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<P> 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();
3131
this.defaultManagedDependentResourceContext =
32-
new DefaultManagedDependentResourceContext<>(controller, primaryResource, this);
32+
new DefaultManagedWorkflowAndDependentResourceContext<>(controller, primaryResource, this);
3333
}
3434

3535
@Override
@@ -80,7 +80,7 @@ public ControllerConfiguration<P> getControllerConfiguration() {
8080
}
8181

8282
@Override
83-
public ManagedDependentResourceContext managedDependentResourceContext() {
83+
public ManagedWorkflowAndDependentResourceContext managedWorkflowAndDependentResourceContext() {
8484
return defaultManagedDependentResourceContext;
8585
}
8686

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

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
Dependent[] dependents();
1414

15-
// todo maybe better naming? "explicitReconciliation" ?
1615
/**
1716
* If true, managed workflow should be explicitly invoked within the reconciler implementation. If
1817
* false workflow is invoked just before the {@link Reconciler#reconcile(HasMetadata, Context)}
+18-7
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowReconcileResult;
1111

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

1616
private final ConcurrentHashMap attributes = new ConcurrentHashMap();
1717
private final Controller<P> controller;
@@ -20,7 +20,7 @@ public class DefaultManagedDependentResourceContext<P extends HasMetadata>
2020
private WorkflowReconcileResult workflowReconcileResult;
2121
private WorkflowCleanupResult workflowCleanupResult;
2222

23-
public DefaultManagedDependentResourceContext(Controller<P> controller,
23+
public DefaultManagedWorkflowAndDependentResourceContext(Controller<P> controller,
2424
P primaryResource,
2525
Context<P> context) {
2626
this.controller = controller;
@@ -52,13 +52,13 @@ public <T> T getMandatory(Object key, Class<T> expectedType) {
5252
+ ") is missing or not of the expected type"));
5353
}
5454

55-
public DefaultManagedDependentResourceContext setWorkflowExecutionResult(
55+
public DefaultManagedWorkflowAndDependentResourceContext setWorkflowExecutionResult(
5656
WorkflowReconcileResult workflowReconcileResult) {
5757
this.workflowReconcileResult = workflowReconcileResult;
5858
return this;
5959
}
6060

61-
public DefaultManagedDependentResourceContext setWorkflowCleanupResult(
61+
public DefaultManagedWorkflowAndDependentResourceContext setWorkflowCleanupResult(
6262
WorkflowCleanupResult workflowCleanupResult) {
6363
this.workflowCleanupResult = workflowCleanupResult;
6464
return this;
@@ -75,8 +75,19 @@ public WorkflowCleanupResult getWorkflowCleanupResult() {
7575
}
7676

7777
@Override
78-
public void invokeWorkflow() {
79-
controller.invokeManagedWorkflow(primaryResource, context);
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);
8091
}
8192

8293
}

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

+4-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
@@ -56,6 +56,8 @@ public interface ManagedDependentResourceContext {
5656

5757
WorkflowCleanupResult getWorkflowCleanupResult();
5858

59-
void invokeWorkflow();
59+
void reconcileManagedWorkflow();
60+
61+
void cleanupManageWorkflow();
6062

6163
}

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

+32-10
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
import io.javaoperatorsdk.operator.RegisteredController;
1919
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
2020
import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager;
21+
import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec;
2122
import io.javaoperatorsdk.operator.api.monitoring.Metrics;
2223
import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution;
2324
import io.javaoperatorsdk.operator.api.reconciler.*;
2425
import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceNotFoundException;
2526
import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider;
2627
import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer;
27-
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext;
28+
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedWorkflowAndDependentResourceContext;
2829
import io.javaoperatorsdk.operator.health.ControllerHealthInfo;
2930
import io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow;
3031
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult;
@@ -131,8 +132,8 @@ public Map<String, Object> metadata() {
131132
public UpdateControl<P> execute() throws Exception {
132133
initContextIfNeeded(resource, context);
133134
configuration.getWorkflowSpec().ifPresent(ws -> {
134-
if (!ws.isExplicitInvocation()) {
135-
invokeManagedWorkflow(resource, context);
135+
if (!isWorkflowExplicitInvocation()) {
136+
reconcileManagedWorkflow(resource, context);
136137
}
137138
});
138139
return reconciler.reconcile(resource, context);
@@ -174,12 +175,14 @@ public Map<String, Object> metadata() {
174175
public DeleteControl execute() {
175176
initContextIfNeeded(resource, context);
176177
WorkflowCleanupResult workflowCleanupResult = null;
177-
if (managedWorkflow.hasCleaner()) {
178-
workflowCleanupResult = managedWorkflow.cleanup(resource, context);
179-
((DefaultManagedDependentResourceContext) context.managedDependentResourceContext())
180-
.setWorkflowCleanupResult(workflowCleanupResult);
181-
workflowCleanupResult.throwAggregateExceptionIfErrorsPresent();
178+
179+
// The cleanup is called also when explicit invocation is true, but the cleaner is not
180+
// implemented
181+
if (!isCleaner
182+
|| !isWorkflowExplicitInvocation()) {
183+
workflowCleanupResult = cleanupManagedWorkflow(resource, context);
182184
}
185+
183186
if (isCleaner) {
184187
var cleanupResult = ((Cleaner<P>) reconciler).cleanup(resource, context);
185188
if (!cleanupResult.isRemoveFinalizer()) {
@@ -429,12 +432,31 @@ public EventSourceContext<P> eventSourceContext() {
429432
return eventSourceContext;
430433
}
431434

432-
public void invokeManagedWorkflow(P primary, Context<P> context) {
435+
public void reconcileManagedWorkflow(P primary, Context<P> context) {
433436
if (!managedWorkflow.isEmpty()) {
434437
var res = managedWorkflow.reconcile(primary, context);
435-
((DefaultManagedDependentResourceContext) context.managedDependentResourceContext())
438+
((DefaultManagedWorkflowAndDependentResourceContext) context
439+
.managedWorkflowAndDependentResourceContext())
436440
.setWorkflowExecutionResult(res);
437441
res.throwAggregateExceptionIfErrorsPresent();
438442
}
439443
}
444+
445+
public WorkflowCleanupResult cleanupManagedWorkflow(P resource, Context<P> context) {
446+
if (managedWorkflow.hasCleaner()) {
447+
var workflowCleanupResult = managedWorkflow.cleanup(resource, context);
448+
((DefaultManagedWorkflowAndDependentResourceContext) context
449+
.managedWorkflowAndDependentResourceContext())
450+
.setWorkflowCleanupResult(workflowCleanupResult);
451+
workflowCleanupResult.throwAggregateExceptionIfErrorsPresent();
452+
return workflowCleanupResult;
453+
} else {
454+
return null;
455+
}
456+
}
457+
458+
public boolean isWorkflowExplicitInvocation() {
459+
return configuration.getWorkflowSpec().map(WorkflowSpec::isExplicitInvocation)
460+
.orElse(false);
461+
}
440462
}
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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.workflowexplicitinvocation.WorkflowExplicitInvocationCustomResource;
10+
import io.javaoperatorsdk.operator.sample.workflowexplicitinvocation.WorkflowExplicitInvocationReconciler;
11+
import io.javaoperatorsdk.operator.sample.workflowexplicitinvocation.WorkflowExplicitInvocationSpec;
12+
13+
import static org.assertj.core.api.Assertions.assertThat;
14+
import static org.awaitility.Awaitility.await;
15+
16+
public class WorkflowExplicitInvocationIT {
17+
18+
public static final String RESOURCE_NAME = "test1";
19+
20+
@RegisterExtension
21+
LocallyRunOperatorExtension extension =
22+
LocallyRunOperatorExtension.builder()
23+
.withReconciler(WorkflowExplicitInvocationReconciler.class)
24+
.build();
25+
26+
@Test
27+
void workflowInvokedExplicitly() {
28+
var res = extension.create(testResource());
29+
var reconciler = extension.getReconcilerOfType(WorkflowExplicitInvocationReconciler.class);
30+
31+
await().untilAsserted(() -> {
32+
assertThat(reconciler.getNumberOfExecutions()).isEqualTo(1);
33+
assertThat(extension.get(ConfigMap.class, RESOURCE_NAME)).isNull();
34+
});
35+
36+
reconciler.setInvokeWorkflow(true);
37+
38+
// trigger reconciliation
39+
res.getSpec().setValue("changed value");
40+
res = extension.replace(res);
41+
42+
await().untilAsserted(() -> {
43+
assertThat(reconciler.getNumberOfExecutions()).isEqualTo(2);
44+
assertThat(extension.get(ConfigMap.class, RESOURCE_NAME)).isNotNull();
45+
});
46+
47+
extension.delete(res);
48+
49+
// The ConfigMap is not garbage collected, this tests that even if the cleaner is not
50+
// implemented the workflow cleanup still called even if there is explicit invocation
51+
await().untilAsserted(() -> {
52+
assertThat(extension.get(ConfigMap.class, RESOURCE_NAME)).isNull();
53+
});
54+
}
55+
56+
WorkflowExplicitInvocationCustomResource testResource() {
57+
var res = new WorkflowExplicitInvocationCustomResource();
58+
res.setMetadata(new ObjectMetaBuilder()
59+
.withName(RESOURCE_NAME)
60+
.build());
61+
res.setSpec(new WorkflowExplicitInvocationSpec());
62+
res.getSpec().setValue("initial value");
63+
return res;
64+
}
65+
66+
}

Diff for: operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ManagedBulkDependentWithReadyConditionReconciler.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public UpdateControl<BulkDependentTestCustomResource> reconcile(
1919
Context<BulkDependentTestCustomResource> context) throws Exception {
2020
numberOfExecutions.incrementAndGet();
2121

22-
var ready = context.managedDependentResourceContext().getWorkflowReconcileResult()
22+
var ready = context.managedWorkflowAndDependentResourceContext().getWorkflowReconcileResult()
2323
.allDependentResourcesReady();
2424

2525

Diff for: operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/complexdependent/ComplexDependentReconciler.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public class ComplexDependentReconciler implements Reconciler<ComplexDependentCu
4040
public UpdateControl<ComplexDependentCustomResource> reconcile(
4141
ComplexDependentCustomResource resource,
4242
Context<ComplexDependentCustomResource> context) throws Exception {
43-
var ready = context.managedDependentResourceContext().getWorkflowReconcileResult()
43+
var ready = context.managedWorkflowAndDependentResourceContext().getWorkflowReconcileResult()
4444
.allDependentResourcesReady();
4545

4646
var status = Objects.requireNonNullElseGet(resource.getStatus(), ComplexDependentStatus::new);

0 commit comments

Comments
 (0)