Skip to content

Commit b77905e

Browse files
csvirimetacosm
andauthored
feat: silent exception handling in managed workflows (#2363)
Signed-off-by: Attila Mészáros <[email protected]> Signed-off-by: Chris Laprun <[email protected]> Co-authored-by: Chris Laprun <[email protected]>
1 parent db61dfb commit b77905e

File tree

11 files changed

+188
-14
lines changed

11 files changed

+188
-14
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ public List<DependentResourceSpec> getDependentResourceSpecs() {
178178
public boolean isExplicitInvocation() {
179179
return workflowAnnotation.explicitInvocation();
180180
}
181+
182+
@Override
183+
public boolean handleExceptionsInReconciler() {
184+
return workflowAnnotation.handleExceptionsInReconciler();
185+
}
186+
181187
};
182188
config.setWorkflowSpec(workflowSpec);
183189
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ public interface WorkflowSpec {
1010
List<DependentResourceSpec> getDependentResourceSpecs();
1111

1212
boolean isExplicitInvocation();
13+
14+
boolean handleExceptionsInReconciler();
1315
}

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

+23-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package io.javaoperatorsdk.operator.api.reconciler;
22

3-
import java.lang.annotation.*;
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Inherited;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
48

59
import io.fabric8.kubernetes.api.model.HasMetadata;
610
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
@@ -13,10 +17,25 @@
1317
Dependent[] dependents();
1418

1519
/**
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.
20+
* If {@code true}, the managed workflow should be explicitly invoked within the reconciler
21+
* implementation. If {@code false}, the workflow is invoked just before the
22+
* {@link Reconciler#reconcile(HasMetadata, Context)} method.
1923
*/
2024
boolean explicitInvocation() default false;
2125

26+
/**
27+
* If {@code true} and exceptions are thrown during the workflow's execution, the reconciler won't
28+
* throw an {@link io.javaoperatorsdk.operator.AggregatedOperatorException} at the end of the
29+
* execution as would normally be the case. Instead, it will proceed to its
30+
* {@link Reconciler#reconcile(HasMetadata, Context)} method as if no error occurred. It is then
31+
* 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.
38+
*/
39+
boolean handleExceptionsInReconciler() default false;
40+
2241
}

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

+8-6
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public class Controller<P extends HasMetadata>
7171
private final boolean isCleaner;
7272
private final Metrics metrics;
7373
private final Workflow<P> managedWorkflow;
74+
private final boolean explicitWorkflowInvocation;
7475

7576
private final GroupVersionKind associatedGVK;
7677
private final EventProcessor<P> eventProcessor;
@@ -93,6 +94,9 @@ public Controller(Reconciler<P> reconciler,
9394

9495
final var managed = configurationService.getWorkflowFactory().workflowFor(configuration);
9596
managedWorkflow = managed.resolve(kubernetesClient, configuration);
97+
explicitWorkflowInvocation =
98+
configuration.getWorkflowSpec().map(WorkflowSpec::isExplicitInvocation)
99+
.orElse(false);
96100

97101
eventSourceManager = new EventSourceManager<>(this);
98102
eventProcessor = new EventProcessor<>(eventSourceManager, configurationService);
@@ -144,7 +148,7 @@ public Map<String, Object> metadata() {
144148
public UpdateControl<P> execute() throws Exception {
145149
initContextIfNeeded(resource, context);
146150
configuration.getWorkflowSpec().ifPresent(ws -> {
147-
if (!isWorkflowExplicitInvocation()) {
151+
if (!explicitWorkflowInvocation) {
148152
reconcileManagedWorkflow(resource, context);
149153
}
150154
});
@@ -190,7 +194,7 @@ public DeleteControl execute() {
190194

191195
// The cleanup is called also when explicit invocation is true, but the cleaner is not
192196
// implemented
193-
if (!isCleaner || !isWorkflowExplicitInvocation()) {
197+
if (!isCleaner || !explicitWorkflowInvocation) {
194198
workflowCleanupResult = cleanupManagedWorkflow(resource, context);
195199
}
196200

@@ -443,7 +447,6 @@ public void reconcileManagedWorkflow(P primary, Context<P> context) {
443447
((DefaultManagedWorkflowAndDependentResourceContext) context
444448
.managedWorkflowAndDependentResourceContext())
445449
.setWorkflowExecutionResult(res);
446-
res.throwAggregateExceptionIfErrorsPresent();
447450
}
448451
}
449452

@@ -453,15 +456,14 @@ public WorkflowCleanupResult cleanupManagedWorkflow(P resource, Context<P> conte
453456
((DefaultManagedWorkflowAndDependentResourceContext) context
454457
.managedWorkflowAndDependentResourceContext())
455458
.setWorkflowCleanupResult(workflowCleanupResult);
456-
workflowCleanupResult.throwAggregateExceptionIfErrorsPresent();
459+
457460
return workflowCleanupResult;
458461
} else {
459462
return null;
460463
}
461464
}
462465

463466
public boolean isWorkflowExplicitInvocation() {
464-
return configuration.getWorkflowSpec().map(WorkflowSpec::isExplicitInvocation)
465-
.orElse(false);
467+
return explicitWorkflowInvocation;
466468
}
467469
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware;
1717

1818
import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_VALUE_SET;
19-
import static io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow.THROW_EXCEPTION_AUTOMATICALLY_DEFAULT;
2019

2120
@SuppressWarnings("rawtypes")
2221
public class DefaultManagedWorkflow<P extends HasMetadata> implements ManagedWorkflow<P> {
@@ -96,7 +95,8 @@ public Workflow<P> resolve(KubernetesClient client,
9695
final var top =
9796
topLevelResources.stream().map(alreadyResolved::get).collect(Collectors.toSet());
9897
return new DefaultWorkflow<>(alreadyResolved, bottom, top,
99-
THROW_EXCEPTION_AUTOMATICALLY_DEFAULT, hasCleaner);
98+
configuration.getWorkflowSpec().map(w -> !w.handleExceptionsInReconciler()).orElseThrow(),
99+
hasCleaner);
100100
}
101101

102102
@SuppressWarnings({"rawtypes", "unchecked"})

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.javaoperatorsdk.operator.processing.dependent.workflow;
22

3+
import java.util.Collections;
34
import java.util.Map;
45
import java.util.Map.Entry;
56
import java.util.stream.Collectors;
@@ -10,11 +11,10 @@
1011
@SuppressWarnings("rawtypes")
1112
class WorkflowResult {
1213

13-
private static final String NUMBER_DELIMITER = "_";
1414
private final Map<DependentResource, Exception> erroredDependents;
1515

1616
WorkflowResult(Map<DependentResource, Exception> erroredDependents) {
17-
this.erroredDependents = erroredDependents;
17+
this.erroredDependents = erroredDependents != null ? erroredDependents : Collections.emptyMap();
1818
}
1919

2020
public Map<DependentResource, Exception> getErroredDependents() {

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

+5
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ public List<DependentResourceSpec> getDependentResourceSpecs() {
7373
public boolean isExplicitInvocation() {
7474
return false;
7575
}
76+
77+
@Override
78+
public boolean handleExceptionsInReconciler() {
79+
return false;
80+
}
7681
};
7782
when(configuration.getWorkflowSpec()).thenReturn(Optional.of(ws));
7883

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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.ObjectMetaBuilder;
7+
import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
8+
import io.javaoperatorsdk.operator.sample.workflowsilentexceptionhandling.HandleWorkflowExceptionsInReconcilerCustomResource;
9+
import io.javaoperatorsdk.operator.sample.workflowsilentexceptionhandling.HandleWorkflowExceptionsInReconcilerReconciler;
10+
11+
import static org.assertj.core.api.Assertions.assertThat;
12+
import static org.awaitility.Awaitility.await;
13+
14+
public class WorkflowSilentExceptionHandlingIT {
15+
16+
@RegisterExtension
17+
LocallyRunOperatorExtension extension =
18+
LocallyRunOperatorExtension.builder()
19+
.withReconciler(HandleWorkflowExceptionsInReconcilerReconciler.class)
20+
.build();
21+
22+
@Test
23+
void handleExceptionsInReconciler() {
24+
extension.create(testResource());
25+
var reconciler =
26+
extension.getReconcilerOfType(HandleWorkflowExceptionsInReconcilerReconciler.class);
27+
28+
await().untilAsserted(() -> {
29+
assertThat(reconciler.isErrorsFoundInReconcilerResult()).isTrue();
30+
});
31+
32+
extension.delete(testResource());
33+
34+
await().untilAsserted(() -> {
35+
assertThat(reconciler.isErrorsFoundInCleanupResult()).isTrue();
36+
});
37+
}
38+
39+
HandleWorkflowExceptionsInReconcilerCustomResource testResource() {
40+
var res = new HandleWorkflowExceptionsInReconcilerCustomResource();
41+
res.setMetadata(new ObjectMetaBuilder()
42+
.withName("test1")
43+
.build());
44+
return res;
45+
}
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package io.javaoperatorsdk.operator.sample.workflowsilentexceptionhandling;
2+
3+
4+
import io.fabric8.kubernetes.api.model.ConfigMap;
5+
import io.javaoperatorsdk.operator.api.reconciler.Context;
6+
import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult;
7+
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDNoGCKubernetesDependentResource;
8+
9+
public class ConfigMapDependent extends
10+
CRUDNoGCKubernetesDependentResource<ConfigMap, HandleWorkflowExceptionsInReconcilerCustomResource> {
11+
12+
public ConfigMapDependent() {
13+
super(ConfigMap.class);
14+
}
15+
16+
@Override
17+
public ReconcileResult<ConfigMap> reconcile(
18+
HandleWorkflowExceptionsInReconcilerCustomResource primary,
19+
Context<HandleWorkflowExceptionsInReconcilerCustomResource> context) {
20+
throw new RuntimeException("Exception thrown on purpose");
21+
}
22+
23+
@Override
24+
public void delete(HandleWorkflowExceptionsInReconcilerCustomResource primary,
25+
Context<HandleWorkflowExceptionsInReconcilerCustomResource> context) {
26+
throw new RuntimeException("Exception thrown on purpose");
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package io.javaoperatorsdk.operator.sample.workflowsilentexceptionhandling;
2+
3+
import io.fabric8.kubernetes.api.model.Namespaced;
4+
import io.fabric8.kubernetes.client.CustomResource;
5+
import io.fabric8.kubernetes.model.annotation.Group;
6+
import io.fabric8.kubernetes.model.annotation.ShortNames;
7+
import io.fabric8.kubernetes.model.annotation.Version;
8+
9+
@Group("sample.javaoperatorsdk")
10+
@Version("v1")
11+
@ShortNames("hweir")
12+
public class HandleWorkflowExceptionsInReconcilerCustomResource
13+
extends CustomResource<Void, Void>
14+
implements Namespaced {
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package io.javaoperatorsdk.operator.sample.workflowsilentexceptionhandling;
2+
3+
import io.javaoperatorsdk.operator.api.reconciler.Cleaner;
4+
import io.javaoperatorsdk.operator.api.reconciler.Context;
5+
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
6+
import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
7+
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
8+
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
9+
import io.javaoperatorsdk.operator.api.reconciler.Workflow;
10+
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
11+
12+
@Workflow(handleExceptionsInReconciler = true,
13+
dependents = @Dependent(type = ConfigMapDependent.class))
14+
@ControllerConfiguration
15+
public class HandleWorkflowExceptionsInReconcilerReconciler
16+
implements Reconciler<HandleWorkflowExceptionsInReconcilerCustomResource>,
17+
Cleaner<HandleWorkflowExceptionsInReconcilerCustomResource> {
18+
19+
private volatile boolean errorsFoundInReconcilerResult = false;
20+
private volatile boolean errorsFoundInCleanupResult = false;
21+
22+
@Override
23+
public UpdateControl<HandleWorkflowExceptionsInReconcilerCustomResource> reconcile(
24+
HandleWorkflowExceptionsInReconcilerCustomResource resource,
25+
Context<HandleWorkflowExceptionsInReconcilerCustomResource> context) {
26+
27+
errorsFoundInReconcilerResult = context.managedWorkflowAndDependentResourceContext()
28+
.getWorkflowReconcileResult().erroredDependentsExist();
29+
30+
31+
return UpdateControl.noUpdate();
32+
}
33+
34+
@Override
35+
public DeleteControl cleanup(HandleWorkflowExceptionsInReconcilerCustomResource resource,
36+
Context<HandleWorkflowExceptionsInReconcilerCustomResource> context) {
37+
38+
errorsFoundInCleanupResult = context.managedWorkflowAndDependentResourceContext()
39+
.getWorkflowCleanupResult().erroredDependentsExist();
40+
return DeleteControl.defaultDelete();
41+
}
42+
43+
public boolean isErrorsFoundInReconcilerResult() {
44+
return errorsFoundInReconcilerResult;
45+
}
46+
47+
public boolean isErrorsFoundInCleanupResult() {
48+
return errorsFoundInCleanupResult;
49+
}
50+
}

0 commit comments

Comments
 (0)