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 specs = dependentResources(workflowAnnotation, config); - WorkflowSpec workflowSpec = new WorkflowSpec(specs); + WorkflowSpec workflowSpec = new WorkflowSpec(specs, workflowAnnotation.explicitInvocation()); config.setWorkflowSpec(workflowSpec); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/workflow/WorkflowSpec.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/workflow/WorkflowSpec.java index f1eea3c5d3..ab89bb07db 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/workflow/WorkflowSpec.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/workflow/WorkflowSpec.java @@ -8,12 +8,19 @@ public class WorkflowSpec { @SuppressWarnings("rawtypes") private final List dependentResourceSpecs; + private final boolean explicitInvocation; - public WorkflowSpec(List dependentResourceSpecs) { + public WorkflowSpec(List dependentResourceSpecs, + boolean explicitInvocation) { this.dependentResourceSpecs = dependentResourceSpecs; + this.explicitInvocation = explicitInvocation; } public List getDependentResourceSpecs() { return dependentResourceSpecs; } + + public boolean isExplicitInvocation() { + return explicitInvocation; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java index 78592495ad..a997835822 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Context.java @@ -8,7 +8,7 @@ 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.ManagedDependentResourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedWorkflowAndDependentResourceContext; import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; import io.javaoperatorsdk.operator.processing.event.source.IndexerResourceCache; @@ -34,7 +34,14 @@ Optional getSecondaryResource(Class expectedType, ControllerConfiguration

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 Optional get(Object key, Class expectedType) { @@ -37,13 +52,13 @@ public T getMandatory(Object key, Class expectedType) { + ") is missing or not of the expected type")); } - public DefaultManagedDependentResourceContext setWorkflowExecutionResult( + public DefaultManagedWorkflowAndDependentResourceContext setWorkflowExecutionResult( WorkflowReconcileResult workflowReconcileResult) { this.workflowReconcileResult = workflowReconcileResult; return this; } - public DefaultManagedDependentResourceContext setWorkflowCleanupResult( + public DefaultManagedWorkflowAndDependentResourceContext setWorkflowCleanupResult( WorkflowCleanupResult workflowCleanupResult) { this.workflowCleanupResult = workflowCleanupResult; return this; @@ -58,4 +73,21 @@ public WorkflowReconcileResult getWorkflowReconcileResult() { public WorkflowCleanupResult getWorkflowCleanupResult() { return workflowCleanupResult; } + + @Override + public void reconcileManagedWorkflow() { + if (!controller.isWorkflowExplicitInvocation()) { + throw new IllegalStateException("Workflow explicit invocation is not set."); + } + controller.reconcileManagedWorkflow(primaryResource, context); + } + + @Override + public void cleanupManageWorkflow() { + if (!controller.isWorkflowExplicitInvocation()) { + throw new IllegalStateException("Workflow explicit invocation is not set."); + } + controller.cleanupManagedWorkflow(primaryResource, context); + } + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedWorkflowAndDependentResourceContext.java similarity index 75% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedWorkflowAndDependentResourceContext.java index 47534cc30d..f5a25a6d9c 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedWorkflowAndDependentResourceContext.java @@ -10,7 +10,7 @@ * Contextual information related to {@link DependentResource} either to retrieve the actual * implementations to interact with them or to pass information between them and/or the reconciler */ -public interface ManagedDependentResourceContext { +public interface ManagedWorkflowAndDependentResourceContext { /** * Retrieve a contextual object, if it exists and is of the specified expected type, associated @@ -37,7 +37,6 @@ public interface ManagedDependentResourceContext { * @return an Optional containing the previous value associated with the key or * {@link Optional#empty()} if none existed */ - @SuppressWarnings("unchecked") T put(Object key, T value); /** @@ -54,5 +53,25 @@ public interface ManagedDependentResourceContext { WorkflowReconcileResult getWorkflowReconcileResult(); + @SuppressWarnings("unused") WorkflowCleanupResult getWorkflowCleanupResult(); + + /** + * Explicitly reconcile the declared workflow for the associated + * {@link io.javaoperatorsdk.operator.api.reconciler.Reconciler} + * + * @throws IllegalStateException if called when explicit invocation is not requested + */ + void reconcileManagedWorkflow(); + + /** + * Explicitly clean-up dependent resources in the declared workflow for the associated + * {@link io.javaoperatorsdk.operator.api.reconciler.Reconciler}. Note that calling this method is + * only needed if the associated reconciler implements the + * {@link io.javaoperatorsdk.operator.api.reconciler.Cleaner} interface. + * + * @throws IllegalStateException if called when explicit invocation is not requested + */ + void cleanupManageWorkflow(); + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java index 78b3a64043..7ab1eca457 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -1,6 +1,11 @@ package io.javaoperatorsdk.operator.processing; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,13 +23,22 @@ import io.javaoperatorsdk.operator.RegisteredController; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; +import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec; import io.javaoperatorsdk.operator.api.monitoring.Metrics; import io.javaoperatorsdk.operator.api.monitoring.Metrics.ControllerExecution; -import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.Cleaner; +import io.javaoperatorsdk.operator.api.reconciler.Constants; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ContextInitializer; +import io.javaoperatorsdk.operator.api.reconciler.DeleteControl; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.api.reconciler.Ignore; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceNotFoundException; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceProvider; import io.javaoperatorsdk.operator.api.reconciler.dependent.EventSourceReferencer; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedWorkflowAndDependentResourceContext; import io.javaoperatorsdk.operator.health.ControllerHealthInfo; import io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow; import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult; @@ -130,12 +144,11 @@ public Map metadata() { @Override public UpdateControl

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 metadata() { public DeleteControl execute() { initContextIfNeeded(resource, context); WorkflowCleanupResult workflowCleanupResult = null; - if (managedWorkflow.hasCleaner()) { - workflowCleanupResult = managedWorkflow.cleanup(resource, context); - ((DefaultManagedDependentResourceContext) context.managedDependentResourceContext()) - .setWorkflowCleanupResult(workflowCleanupResult); - workflowCleanupResult.throwAggregateExceptionIfErrorsPresent(); + + // The cleanup is called also when explicit invocation is true, but the cleaner is not + // implemented + if (!isCleaner || !isWorkflowExplicitInvocation()) { + workflowCleanupResult = cleanupManagedWorkflow(resource, context); } + if (isCleaner) { var cleanupResult = ((Cleaner

) 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 reconcile( Context context) throws Exception { numberOfExecutions.incrementAndGet(); - var ready = context.managedDependentResourceContext().getWorkflowReconcileResult() + var ready = context.managedWorkflowAndDependentResourceContext().getWorkflowReconcileResult() .allDependentResourcesReady(); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/complexdependent/ComplexDependentReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/complexdependent/ComplexDependentReconciler.java index e8fa40c63e..81bcb7e153 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/complexdependent/ComplexDependentReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/complexdependent/ComplexDependentReconciler.java @@ -40,7 +40,7 @@ public class ComplexDependentReconciler implements Reconciler reconcile( ComplexDependentCustomResource resource, Context context) throws Exception { - var ready = context.managedDependentResourceContext().getWorkflowReconcileResult() + var ready = context.managedWorkflowAndDependentResourceContext().getWorkflowReconcileResult() .allDependentResourcesReady(); var status = Objects.requireNonNullElseGet(resource.getStatus(), ComplexDependentStatus::new); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureReconciler.java index 1fadcdad66..f5c14d6f96 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureReconciler.java @@ -35,7 +35,7 @@ public UpdateControl reconcile( } resource.getStatus() .setReady( - context.managedDependentResourceContext() + context.managedWorkflowAndDependentResourceContext() .getWorkflowReconcileResult() .allDependentResourcesReady()); return UpdateControl.patchStatus(resource); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/ConfigMapDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/ConfigMapDependent.java new file mode 100644 index 0000000000..91bf73906f --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/ConfigMapDependent.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.operator.sample.workflowexplicitcleanup; + +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDNoGCKubernetesDependentResource; + +public class ConfigMapDependent extends + CRUDNoGCKubernetesDependentResource { + + public ConfigMapDependent() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(WorkflowExplicitCleanupCustomResource primary, + Context context) { + return new ConfigMapBuilder() + .withMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()) + .withData(Map.of("key", "val")) + .build(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/WorkflowExplicitCleanupCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/WorkflowExplicitCleanupCustomResource.java new file mode 100644 index 0000000000..b2057a54dd --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/WorkflowExplicitCleanupCustomResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.workflowexplicitcleanup; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("wec") +public class WorkflowExplicitCleanupCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/WorkflowExplicitCleanupReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/WorkflowExplicitCleanupReconciler.java new file mode 100644 index 0000000000..128bb9629c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/WorkflowExplicitCleanupReconciler.java @@ -0,0 +1,32 @@ +package io.javaoperatorsdk.operator.sample.workflowexplicitcleanup; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +@Workflow(explicitInvocation = true, + dependents = @Dependent(type = ConfigMapDependent.class)) +@ControllerConfiguration +public class WorkflowExplicitCleanupReconciler + implements Reconciler, + Cleaner { + + @Override + public UpdateControl reconcile( + WorkflowExplicitCleanupCustomResource resource, + Context context) { + + context.managedWorkflowAndDependentResourceContext().reconcileManagedWorkflow(); + + return UpdateControl.noUpdate(); + } + + @Override + public DeleteControl cleanup(WorkflowExplicitCleanupCustomResource resource, + Context context) { + + context.managedWorkflowAndDependentResourceContext().cleanupManageWorkflow(); + // this can be checked + // context.managedWorkflowAndDependentResourceContext().getWorkflowCleanupResult() + return DeleteControl.defaultDelete(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/WorkflowExplicitCleanupSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/WorkflowExplicitCleanupSpec.java new file mode 100644 index 0000000000..d8da8797f5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitcleanup/WorkflowExplicitCleanupSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.workflowexplicitcleanup; + +public class WorkflowExplicitCleanupSpec { + + private String value; + + public String getValue() { + return value; + } + + public WorkflowExplicitCleanupSpec setValue(String value) { + this.value = value; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/ConfigMapDependent.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/ConfigMapDependent.java new file mode 100644 index 0000000000..e26fcfcf11 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/ConfigMapDependent.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.operator.sample.workflowexplicitinvocation; + +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDNoGCKubernetesDependentResource; + +public class ConfigMapDependent extends + CRUDNoGCKubernetesDependentResource { + + public ConfigMapDependent() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(WorkflowExplicitInvocationCustomResource primary, + Context context) { + return new ConfigMapBuilder() + .withMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()) + .withData(Map.of("key", primary.getSpec().getValue())) + .build(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/WorkflowExplicitInvocationCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/WorkflowExplicitInvocationCustomResource.java new file mode 100644 index 0000000000..827a17ddaf --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/WorkflowExplicitInvocationCustomResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.workflowexplicitinvocation; + +import io.fabric8.kubernetes.api.model.Namespaced; +import io.fabric8.kubernetes.client.CustomResource; +import io.fabric8.kubernetes.model.annotation.Group; +import io.fabric8.kubernetes.model.annotation.ShortNames; +import io.fabric8.kubernetes.model.annotation.Version; + +@Group("sample.javaoperatorsdk") +@Version("v1") +@ShortNames("wei") +public class WorkflowExplicitInvocationCustomResource + extends CustomResource + implements Namespaced { +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/WorkflowExplicitInvocationReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/WorkflowExplicitInvocationReconciler.java new file mode 100644 index 0000000000..dc7bce4296 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/WorkflowExplicitInvocationReconciler.java @@ -0,0 +1,39 @@ +package io.javaoperatorsdk.operator.sample.workflowexplicitinvocation; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +@Workflow(explicitInvocation = true, + dependents = @Dependent(type = ConfigMapDependent.class)) +@ControllerConfiguration +public class WorkflowExplicitInvocationReconciler + implements Reconciler { + + private final AtomicInteger numberOfExecutions = new AtomicInteger(0); + + private volatile boolean invokeWorkflow = false; + + @Override + public UpdateControl reconcile( + WorkflowExplicitInvocationCustomResource resource, + Context context) { + + numberOfExecutions.addAndGet(1); + if (invokeWorkflow) { + context.managedWorkflowAndDependentResourceContext().reconcileManagedWorkflow(); + } + + + return UpdateControl.noUpdate(); + } + + public int getNumberOfExecutions() { + return numberOfExecutions.get(); + } + + public void setInvokeWorkflow(boolean invokeWorkflow) { + this.invokeWorkflow = invokeWorkflow; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/WorkflowExplicitInvocationSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/WorkflowExplicitInvocationSpec.java new file mode 100644 index 0000000000..2112d348e2 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowexplicitinvocation/WorkflowExplicitInvocationSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.workflowexplicitinvocation; + +public class WorkflowExplicitInvocationSpec { + + private String value; + + public String getValue() { + return value; + } + + public WorkflowExplicitInvocationSpec setValue(String value) { + this.value = value; + return this; + } +}