Skip to content

Commit 92d572a

Browse files
csvirimetacosm
andcommitted
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]> Signed-off-by: Attila Mészáros <[email protected]>
1 parent 2f30de3 commit 92d572a

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
@@ -70,6 +70,7 @@ public class Controller<P extends HasMetadata>
7070
private final boolean isCleaner;
7171
private final Metrics metrics;
7272
private final Workflow<P> managedWorkflow;
73+
private final boolean explicitWorkflowInvocation;
7374

7475
private final GroupVersionKind associatedGVK;
7576
private final EventProcessor<P> eventProcessor;
@@ -92,6 +93,9 @@ public Controller(Reconciler<P> reconciler,
9293

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

96100
eventSourceManager = new EventSourceManager<>(this);
97101
eventProcessor = new EventProcessor<>(eventSourceManager, configurationService);
@@ -143,7 +147,7 @@ public Map<String, Object> metadata() {
143147
public UpdateControl<P> execute() throws Exception {
144148
initContextIfNeeded(resource, context);
145149
configuration.getWorkflowSpec().ifPresent(ws -> {
146-
if (!managedWorkflow.isEmpty() && !isWorkflowExplicitInvocation()) {
150+
if (!managedWorkflow.isEmpty() && !explicitWorkflowInvocation) {
147151
managedWorkflow.reconcile(resource, context);
148152
}
149153
});
@@ -188,7 +192,7 @@ public DeleteControl execute() {
188192
WorkflowCleanupResult workflowCleanupResult = null;
189193

190194
// The cleanup is called also when explicit invocation is true, but the cleaner is not implemented
191-
if (managedWorkflow.hasCleaner() || !isWorkflowExplicitInvocation()) {
195+
if (managedWorkflow.hasCleaner() || !explicitWorkflowInvocation) {
192196
workflowCleanupResult = managedWorkflow.cleanup(resource, context);
193197
}
194198

@@ -449,7 +453,6 @@ public void reconcileManagedWorkflow(P primary, Context<P> context) {
449453
((DefaultManagedWorkflowAndDependentResourceContext) context
450454
.managedWorkflowAndDependentResourceContext())
451455
.setWorkflowExecutionResult(res);
452-
res.throwAggregateExceptionIfErrorsPresent();
453456
}
454457
}
455458

@@ -459,15 +462,14 @@ public WorkflowCleanupResult cleanupManagedWorkflow(P resource, Context<P> conte
459462
((DefaultManagedWorkflowAndDependentResourceContext) context
460463
.managedWorkflowAndDependentResourceContext())
461464
.setWorkflowCleanupResult(workflowCleanupResult);
462-
workflowCleanupResult.throwAggregateExceptionIfErrorsPresent();
465+
463466
return workflowCleanupResult;
464467
} else {
465468
return null;
466469
}
467470
}
468471

469472
public boolean isWorkflowExplicitInvocation() {
470-
return configuration.getWorkflowSpec().map(WorkflowSpec::isExplicitInvocation)
471-
.orElse(false);
473+
return explicitWorkflowInvocation;
472474
}
473475
}

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)