Skip to content

feat: explicit workflow invocation #2289

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/documentation/v5-0-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
28 changes: 28 additions & 0 deletions docs/documentation/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ protected <P extends HasMetadata> ControllerConfiguration<P> configFor(Reconcile
io.javaoperatorsdk.operator.api.reconciler.Workflow.class);
if (workflowAnnotation != null) {
List<DependentResourceSpec> specs = dependentResources(workflowAnnotation, config);
WorkflowSpec workflowSpec = new WorkflowSpec(specs);
WorkflowSpec workflowSpec = new WorkflowSpec(specs, workflowAnnotation.explicitInvocation());
config.setWorkflowSpec(workflowSpec);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,19 @@ public class WorkflowSpec {

@SuppressWarnings("rawtypes")
private final List<DependentResourceSpec> dependentResourceSpecs;
private final boolean explicitInvocation;

public WorkflowSpec(List<DependentResourceSpec> dependentResourceSpecs) {
public WorkflowSpec(List<DependentResourceSpec> dependentResourceSpecs,
boolean explicitInvocation) {
this.dependentResourceSpecs = dependentResourceSpecs;
this.explicitInvocation = explicitInvocation;
}

public List<DependentResourceSpec> getDependentResourceSpecs() {
return dependentResourceSpecs;
}

public boolean isExplicitInvocation() {
return explicitInvocation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -34,7 +34,14 @@ <R> Optional<R> getSecondaryResource(Class<R> expectedType,

ControllerConfiguration<P> 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<P> eventSourceRetriever();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,14 +21,15 @@ public class DefaultContext<P extends HasMetadata> implements Context<P> {
private final Controller<P> controller;
private final P primaryResource;
private final ControllerConfiguration<P> controllerConfiguration;
private final DefaultManagedDependentResourceContext defaultManagedDependentResourceContext;
private final DefaultManagedWorkflowAndDependentResourceContext<P> defaultManagedDependentResourceContext;

public DefaultContext(RetryInfo retryInfo, Controller<P> 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
Expand Down Expand Up @@ -79,7 +80,7 @@ public ControllerConfiguration<P> getControllerConfiguration() {
}

@Override
public ManagedDependentResourceContext managedDependentResourceContext() {
public ManagedWorkflowAndDependentResourceContext managedWorkflowAndDependentResourceContext() {
return defaultManagedDependentResourceContext;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.lang.annotation.*;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent;

@Inherited
Expand All @@ -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;

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<P extends HasMetadata>
implements ManagedWorkflowAndDependentResourceContext {

private final ConcurrentHashMap attributes = new ConcurrentHashMap();
private final Controller<P> controller;
private final P primaryResource;
private final Context<P> context;
private WorkflowReconcileResult workflowReconcileResult;
private WorkflowCleanupResult workflowCleanupResult;
private final ConcurrentHashMap attributes = new ConcurrentHashMap();

public DefaultManagedWorkflowAndDependentResourceContext(Controller<P> controller,
P primaryResource,
Context<P> context) {
this.controller = controller;
this.primaryResource = primaryResource;
this.context = context;
}

@Override
public <T> Optional<T> get(Object key, Class<T> expectedType) {
Expand All @@ -37,13 +52,13 @@ public <T> T getMandatory(Object key, Class<T> 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;
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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> T put(Object key, T value);

/**
Expand All @@ -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();

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -130,12 +144,11 @@ public Map<String, Object> metadata() {
@Override
public UpdateControl<P> 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);
}
});
Expand Down Expand Up @@ -175,12 +188,13 @@ public Map<String, Object> 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<P>) reconciler).cleanup(resource, context);
if (!cleanupResult.isRemoveFinalizer()) {
Expand Down Expand Up @@ -429,4 +443,32 @@ public ExecutorServiceManager getExecutorServiceManager() {
public EventSourceContext<P> eventSourceContext() {
return eventSourceContext;
}

public void reconcileManagedWorkflow(P primary, Context<P> context) {
if (!managedWorkflow.isEmpty()) {
var res = managedWorkflow.reconcile(primary, context);
((DefaultManagedWorkflowAndDependentResourceContext) context
.managedWorkflowAndDependentResourceContext())
.setWorkflowExecutionResult(res);
res.throwAggregateExceptionIfErrorsPresent();
}
}

public WorkflowCleanupResult cleanupManagedWorkflow(P resource, Context<P> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading
Loading