diff --git a/docs/documentation/v5-0-migration.md b/docs/documentation/v5-0-migration.md index f4ec51f4fb..f69625afad 100644 --- a/docs/documentation/v5-0-migration.md +++ b/docs/documentation/v5-0-migration.md @@ -17,3 +17,5 @@ permalink: /docs/v5-0-migration [`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) now contains all the utility methods used for event sources naming that were previously defined in the `EventSourceInitializer` interface. +3. `ManagedDependentResourceContext` has been renamed to `ManagedWorkflowAndDependentResourceContext` and is accessed + via the accordingly renamed `managedWorkflowAndDependentResourceContext` method. diff --git a/docs/documentation/workflows.md b/docs/documentation/workflows.md index d0d7e5b2c7..ea83016430 100644 --- a/docs/documentation/workflows.md +++ b/docs/documentation/workflows.md @@ -338,6 +338,34 @@ and NOT `CRUDKubernetesDependentResource` since otherwise the Kubernetes Garbage In other words if a Kubernetes Dependent Resource depends on another dependent resource, it should not implement `GargageCollected` interface, otherwise the deletion order won't be guaranteed. + +## Explicit Managed Workflow Invocation + +Managed workflows, i.e. ones that are declared via annotations and therefore completely managed by JOSDK, are reconciled +before the primary resource. Each dependent resource that can be reconciled (according to the workflow configuration) +will therefore be reconciled before the primary reconciler is called to reconcile the primary resource. There are, +however, situations where it would be be useful to perform additional steps before the workflow is reconciled, for +example to validate the current state, execute arbitrary logic or even skip reconciliation altogether. Explicit +invocation of managed workflow was therefore introduced to solve these issues. + +To use this feature, you need to set the `explicitInvocation` field to `true` on the `@Workflow` annotation and then +call the `reconcileManagedWorkflow` method from the ` +ManagedWorkflowAndDependentResourceContext` retrieved from the reconciliation `Context` provided as part of your primary +resource reconciler `reconcile` method arguments. + +See +related [integration test](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowExplicitInvocationIT.java) +for more details. + +For `cleanup`, if the `Cleaner` interface is implemented, the `cleanupManageWorkflow()` needs to be called explicitly. +However, if `Cleaner` interface is not implemented, it will be called implicitly. +See +related [integration test](https://github.com/operator-framework/java-operator-sdk/blob/main/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowExplicitCleanupIT.java). + +While nothing prevents calling the workflow multiple times in a reconciler, it isn't typical or even recommended to do +so. Conversely, if explicit invocation is requested but `reconcileManagedWorkflow` is not called in the primary resource +reconciler, the workflow won't be reconciled at all. + ## Notes and Caveats - Delete is almost always called on every resource during the cleanup. However, it might be the case diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java index eab8e61f72..846c16046c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/BaseConfigurationService.java @@ -169,7 +169,7 @@ protected
ControllerConfiguration
configFor(Reconcile
io.javaoperatorsdk.operator.api.reconciler.Workflow.class);
if (workflowAnnotation != null) {
List getControllerConfiguration();
- ManagedDependentResourceContext managedDependentResourceContext();
+ /**
+ * Retrieve the {@link ManagedWorkflowAndDependentResourceContext} used to interact with
+ * {@link io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource}s and associated
+ * {@link io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow}
+ *
+ * @return the {@link ManagedWorkflowAndDependentResourceContext}
+ */
+ ManagedWorkflowAndDependentResourceContext managedWorkflowAndDependentResourceContext();
EventSourceRetriever eventSourceRetriever();
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java
index 633daea6aa..9ff7ddd7a3 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java
@@ -9,8 +9,8 @@
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
-import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext;
-import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedWorkflowAndDependentResourceContext;
+import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext;
import io.javaoperatorsdk.operator.processing.Controller;
import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
@@ -21,14 +21,15 @@ public class DefaultContext implements Context {
private final Controller controller;
private final P primaryResource;
private final ControllerConfiguration controllerConfiguration;
- private final DefaultManagedDependentResourceContext defaultManagedDependentResourceContext;
+ private final DefaultManagedWorkflowAndDependentResourceContext defaultManagedDependentResourceContext;
public DefaultContext(RetryInfo retryInfo, Controller controller, P primaryResource) {
this.retryInfo = retryInfo;
this.controller = controller;
this.primaryResource = primaryResource;
this.controllerConfiguration = controller.getConfiguration();
- this.defaultManagedDependentResourceContext = new DefaultManagedDependentResourceContext();
+ this.defaultManagedDependentResourceContext =
+ new DefaultManagedWorkflowAndDependentResourceContext<>(controller, primaryResource, this);
}
@Override
@@ -79,7 +80,7 @@ public ControllerConfiguration getControllerConfiguration() {
}
@Override
- public ManagedDependentResourceContext managedDependentResourceContext() {
+ public ManagedWorkflowAndDependentResourceContext managedWorkflowAndDependentResourceContext() {
return defaultManagedDependentResourceContext;
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Workflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Workflow.java
index 6726a1d32b..04a5b21606 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Workflow.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Workflow.java
@@ -2,6 +2,7 @@
import java.lang.annotation.*;
+import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;
@Inherited
@@ -11,4 +12,11 @@
Dependent[] dependents();
+ /**
+ * If true, managed workflow should be explicitly invoked within the reconciler implementation. If
+ * false workflow is invoked just before the {@link Reconciler#reconcile(HasMetadata, Context)}
+ * method.
+ */
+ boolean explicitInvocation() default false;
+
}
diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DefaultManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DefaultManagedWorkflowAndDependentResourceContext.java
similarity index 56%
rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DefaultManagedDependentResourceContext.java
rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DefaultManagedWorkflowAndDependentResourceContext.java
index d6fa5c7b32..f93f45ecf7 100644
--- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DefaultManagedDependentResourceContext.java
+++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DefaultManagedWorkflowAndDependentResourceContext.java
@@ -3,15 +3,30 @@
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
+import io.fabric8.kubernetes.api.model.HasMetadata;
+import io.javaoperatorsdk.operator.api.reconciler.Context;
+import io.javaoperatorsdk.operator.processing.Controller;
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult;
import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowReconcileResult;
@SuppressWarnings("rawtypes")
-public class DefaultManagedDependentResourceContext implements ManagedDependentResourceContext {
+public class DefaultManagedWorkflowAndDependentResourceContext
+ implements ManagedWorkflowAndDependentResourceContext {
+ private final ConcurrentHashMap attributes = new ConcurrentHashMap();
+ private final Controller controller;
+ private final P primaryResource;
+ private final Context context;
private WorkflowReconcileResult workflowReconcileResult;
private WorkflowCleanupResult workflowCleanupResult;
- private final ConcurrentHashMap attributes = new ConcurrentHashMap();
+
+ public DefaultManagedWorkflowAndDependentResourceContext(Controller controller,
+ P primaryResource,
+ Context context) {
+ this.controller = controller;
+ this.primaryResource = primaryResource;
+ this.context = context;
+ }
@Override
public execute() throws Exception {
initContextIfNeeded(resource, context);
- if (!managedWorkflow.isEmpty()) {
- var res = managedWorkflow.reconcile(resource, context);
- ((DefaultManagedDependentResourceContext) context.managedDependentResourceContext())
- .setWorkflowExecutionResult(res);
- res.throwAggregateExceptionIfErrorsPresent();
- }
+ configuration.getWorkflowSpec().ifPresent(ws -> {
+ if (!isWorkflowExplicitInvocation()) {
+ reconcileManagedWorkflow(resource, context);
+ }
+ });
return reconciler.reconcile(resource, context);
}
});
@@ -175,12 +188,13 @@ public Map ) reconciler).cleanup(resource, context);
if (!cleanupResult.isRemoveFinalizer()) {
@@ -429,4 +443,32 @@ public ExecutorServiceManager getExecutorServiceManager() {
public EventSourceContext eventSourceContext() {
return eventSourceContext;
}
+
+ public void reconcileManagedWorkflow(P primary, Context context) {
+ if (!managedWorkflow.isEmpty()) {
+ var res = managedWorkflow.reconcile(primary, context);
+ ((DefaultManagedWorkflowAndDependentResourceContext) context
+ .managedWorkflowAndDependentResourceContext())
+ .setWorkflowExecutionResult(res);
+ res.throwAggregateExceptionIfErrorsPresent();
+ }
+ }
+
+ public WorkflowCleanupResult cleanupManagedWorkflow(P resource, Context context) {
+ if (managedWorkflow.hasCleaner()) {
+ var workflowCleanupResult = managedWorkflow.cleanup(resource, context);
+ ((DefaultManagedWorkflowAndDependentResourceContext) context
+ .managedWorkflowAndDependentResourceContext())
+ .setWorkflowCleanupResult(workflowCleanupResult);
+ workflowCleanupResult.throwAggregateExceptionIfErrorsPresent();
+ return workflowCleanupResult;
+ } else {
+ return null;
+ }
+ }
+
+ public boolean isWorkflowExplicitInvocation() {
+ return configuration.getWorkflowSpec().map(WorkflowSpec::isExplicitInvocation)
+ .orElse(false);
+ }
}
diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java
index 5327439a31..fc254aa217 100644
--- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java
+++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java
@@ -64,7 +64,7 @@ ManagedWorkflow managedWorkflow(DependentResourceSpec... specs) {
final var configuration = mock(ControllerConfiguration.class);
final var specList = List.of(specs);
- var ws = new WorkflowSpec(specList);
+ var ws = new WorkflowSpec(specList, false);
when(configuration.getWorkflowSpec()).thenReturn(Optional.of(ws));
return new BaseConfigurationService().getWorkflowFactory()
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowExplicitCleanupIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowExplicitCleanupIT.java
new file mode 100644
index 0000000000..b26bfdd443
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowExplicitCleanupIT.java
@@ -0,0 +1,50 @@
+package io.javaoperatorsdk.operator;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
+import io.javaoperatorsdk.operator.sample.workflowexplicitcleanup.WorkflowExplicitCleanupCustomResource;
+import io.javaoperatorsdk.operator.sample.workflowexplicitcleanup.WorkflowExplicitCleanupReconciler;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+public class WorkflowExplicitCleanupIT {
+
+ public static final String RESOURCE_NAME = "test1";
+
+ @RegisterExtension
+ LocallyRunOperatorExtension extension =
+ LocallyRunOperatorExtension.builder()
+ .withReconciler(WorkflowExplicitCleanupReconciler.class)
+ .build();
+
+ @Test
+ void workflowInvokedExplicitly() {
+ var res = extension.create(testResource());
+
+ await().untilAsserted(() -> {
+ assertThat(extension.get(ConfigMap.class, RESOURCE_NAME)).isNotNull();
+ });
+
+ extension.delete(res);
+
+ // The ConfigMap is not garbage collected, this tests that even if the cleaner is not
+ // implemented the workflow cleanup still called even if there is explicit invocation
+ await().untilAsserted(() -> {
+ assertThat(extension.get(ConfigMap.class, RESOURCE_NAME)).isNull();
+ });
+ }
+
+ WorkflowExplicitCleanupCustomResource testResource() {
+ var res = new WorkflowExplicitCleanupCustomResource();
+ res.setMetadata(new ObjectMetaBuilder()
+ .withName(RESOURCE_NAME)
+ .build());
+ return res;
+ }
+
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowExplicitInvocationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowExplicitInvocationIT.java
new file mode 100644
index 0000000000..dba08faba0
--- /dev/null
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowExplicitInvocationIT.java
@@ -0,0 +1,66 @@
+package io.javaoperatorsdk.operator;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
+import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension;
+import io.javaoperatorsdk.operator.sample.workflowexplicitinvocation.WorkflowExplicitInvocationCustomResource;
+import io.javaoperatorsdk.operator.sample.workflowexplicitinvocation.WorkflowExplicitInvocationReconciler;
+import io.javaoperatorsdk.operator.sample.workflowexplicitinvocation.WorkflowExplicitInvocationSpec;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.await;
+
+public class WorkflowExplicitInvocationIT {
+
+ public static final String RESOURCE_NAME = "test1";
+
+ @RegisterExtension
+ LocallyRunOperatorExtension extension =
+ LocallyRunOperatorExtension.builder()
+ .withReconciler(WorkflowExplicitInvocationReconciler.class)
+ .build();
+
+ @Test
+ void workflowInvokedExplicitly() {
+ var res = extension.create(testResource());
+ var reconciler = extension.getReconcilerOfType(WorkflowExplicitInvocationReconciler.class);
+
+ await().untilAsserted(() -> {
+ assertThat(reconciler.getNumberOfExecutions()).isEqualTo(1);
+ assertThat(extension.get(ConfigMap.class, RESOURCE_NAME)).isNull();
+ });
+
+ reconciler.setInvokeWorkflow(true);
+
+ // trigger reconciliation
+ res.getSpec().setValue("changed value");
+ res = extension.replace(res);
+
+ await().untilAsserted(() -> {
+ assertThat(reconciler.getNumberOfExecutions()).isEqualTo(2);
+ assertThat(extension.get(ConfigMap.class, RESOURCE_NAME)).isNotNull();
+ });
+
+ extension.delete(res);
+
+ // The ConfigMap is not garbage collected, this tests that even if the cleaner is not
+ // implemented the workflow cleanup still called even if there is explicit invocation
+ await().untilAsserted(() -> {
+ assertThat(extension.get(ConfigMap.class, RESOURCE_NAME)).isNull();
+ });
+ }
+
+ WorkflowExplicitInvocationCustomResource testResource() {
+ var res = new WorkflowExplicitInvocationCustomResource();
+ res.setMetadata(new ObjectMetaBuilder()
+ .withName(RESOURCE_NAME)
+ .build());
+ res.setSpec(new WorkflowExplicitInvocationSpec());
+ res.getSpec().setValue("initial value");
+ return res;
+ }
+
+}
diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ManagedBulkDependentWithReadyConditionReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ManagedBulkDependentWithReadyConditionReconciler.java
index 8da3ba944f..aca1e98c88 100644
--- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ManagedBulkDependentWithReadyConditionReconciler.java
+++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/bulkdependent/ManagedBulkDependentWithReadyConditionReconciler.java
@@ -19,7 +19,7 @@ public UpdateControl