diff --git a/docs/documentation/workflows.md b/docs/documentation/workflows.md index c676207855..fed30afb25 100644 --- a/docs/documentation/workflows.md +++ b/docs/documentation/workflows.md @@ -35,6 +35,14 @@ reconciliation process. proceeding until the condition checking whether the DR is ready holds true - **Delete postcondition** - is a condition on a given DR to check if the reconciliation of dependents can proceed after the DR is supposed to have been deleted +- **Activation condition** - is a special condition meant to specify under which condition the DR is used in the + workflow. A typical use-case for this feature is to only activate some dependents depending on the presence of + optional resources / features on the target cluster. Without this activation condition, JOSDK would attempt to + register an informer for these optional resources, which would cause an error in the case where the resource is + missing. With this activation condition, you can now conditionally register informers depending on whether the + condition holds or not. This is a very useful feature when your operator needs to handle different flavors of the + platform (e.g. OpenShift vs plain Kubernetes) and/or change its behavior based on the availability of optional + resources / features (e.g. CertManager, a specific Ingress controller, etc.). ## Defining Workflows @@ -66,6 +74,7 @@ will only consider the `ConfigMap` deleted until that post-condition becomes `tr @Dependent(type = ConfigMapDependentResource.class, reconcilePrecondition = ConfigMapReconcileCondition.class, deletePostcondition = ConfigMapDeletePostCondition.class, + activationCondition = ConfigMapActivationCondition.class, dependsOn = DEPLOYMENT_NAME) }) public class SampleWorkflowReconciler implements Reconciler, @@ -165,7 +174,7 @@ executed if a resource is marked for deletion. ## Common Principles - **As complete as possible execution** - when a workflow is reconciled, it tries to reconcile as - many resources as possible. Thus if an error happens or a ready condition is not met for a + many resources as possible. Thus, if an error happens or a ready condition is not met for a resources, all the other independent resources will be still reconciled. This is the opposite to a fail-fast approach. The assumption is that eventually in this way the overall state will converge faster towards the desired state than would be the case if the reconciliation was @@ -186,13 +195,13 @@ demonstrated using examples: `depends-on` relations. 2. Root nodes, i.e. nodes in the graph that do not depend on other nodes are reconciled first, in a parallel manner. -2. A DR is reconciled if it does not depend on any other DRs, or *ALL* the DRs it depends on are - reconciled and ready. If a DR defines a reconcile pre-condition, then this condition must - become `true` before the DR is reconciled. -2. A DR is considered *ready* if it got successfully reconciled and any ready post-condition it +3. A DR is reconciled if it does not depend on any other DRs, or *ALL* the DRs it depends on are + reconciled and ready. If a DR defines a reconcile pre-condition and/or an activation condition, + then these condition must become `true` before the DR is reconciled. +4. A DR is considered *ready* if it got successfully reconciled and any ready post-condition it might define is `true`. -3. If a DR's reconcile pre-condition is not met, this DR is deleted. All of the DRs that depend - on the dependent resource being considered are also recursively deleted. This implies that +5. If a DR's reconcile pre-condition is not met, this DR is deleted. All the DRs that depend + on the dependent resource are also recursively deleted. This implies that DRs are deleted in reverse order compared the one in which they are reconciled. The reason for this behavior is (Will make a more detailed blog post about the design decision, much deeper than the reference documentation) @@ -202,7 +211,10 @@ demonstrated using examples: idempotency (i.e. with the same input state, we should have the same output state), from this follows that if the condition doesn't hold `true` anymore, the associated resource needs to be deleted because the resource shouldn't exist/have been created. -4. For a DR to be deleted by a workflow, it needs to implement the `Deleter` interface, in which +6. If a DR's activation condition is not met, it won't be reconciled or deleted. If other DR's depend on it, those will + be recursively deleted in a way similar to reconcile pre-conditions. Event sources for a dependent resource with + activation condition are registered/de-registered dynamically, thus during the reconciliation. +7. For a DR to be deleted by a workflow, it needs to implement the `Deleter` interface, in which case its `delete` method will be called, unless it also implements the `GarbageCollected` interface. If a DR doesn't implement `Deleter` it is considered as automatically deleted. If a delete post-condition exists for this DR, it needs to become `true` for the workflow to 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 ae836f2a72..d908f52ebe 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 @@ -230,6 +230,7 @@ private static List dependentResources( Utils.instantiate(dependent.readyPostcondition(), Condition.class, context), Utils.instantiate(dependent.reconcilePrecondition(), Condition.class, context), Utils.instantiate(dependent.deletePostcondition(), Condition.class, context), + Utils.instantiate(dependent.activationCondition(), Condition.class, context), eventSourceName); specsMap.put(dependentName, spec); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java index 58fd9ace4b..1fcd0709fb 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/dependent/DependentResourceSpec.java @@ -22,18 +22,21 @@ public class DependentResourceSpec { private final Condition deletePostCondition; + private final Condition activationCondition; + private final String useEventSourceWithName; public DependentResourceSpec(Class> dependentResourceClass, String name, Set dependsOn, Condition readyCondition, Condition reconcileCondition, Condition deletePostCondition, - String useEventSourceWithName) { + Condition activationCondition, String useEventSourceWithName) { this.dependentResourceClass = dependentResourceClass; this.name = name; this.dependsOn = dependsOn; this.readyCondition = readyCondition; this.reconcileCondition = reconcileCondition; this.deletePostCondition = deletePostCondition; + this.activationCondition = activationCondition; this.useEventSourceWithName = useEventSourceWithName; } @@ -87,6 +90,11 @@ public Condition getDeletePostCondition() { return deletePostCondition; } + @SuppressWarnings("rawtypes") + public Condition getActivationCondition() { + return activationCondition; + } + public Optional getUseEventSourceWithName() { return Optional.ofNullable(useEventSourceWithName); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java index 78e9ee4581..754e2c85be 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/Dependent.java @@ -50,6 +50,26 @@ */ Class deletePostcondition() default Condition.class; + /** + *

+ * A condition that needs to become true for the dependent to even be considered as part of the + * workflow. This is useful for dependents that represent optional resources on the cluster and + * might not be present. In this case, a reconcile pre-condition is not enough because in that + * situation, the associated informer will still be registered. With this activation condition, + * the associated event source will only be registered if the condition is met. Otherwise, this + * behaves like a reconcile pre-condition in the sense that dependents, that depend on this one, + * will only get created if the condition is met and will get deleted if the condition becomes + * false. + *

+ *

+ * As other conditions, this gets evaluated at the beginning of every reconciliation, which means + * that it allows to react to optional resources becoming available on the cluster as the operator + * runs. More specifically, this means that the associated event source can get dynamically + * registered or de-registered during reconciliation. + *

+ */ + Class activationCondition() default Condition.class; + /** * The list of named dependents that need to be reconciled before this one can be. * 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 e8d15671bf..70069148c3 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 @@ -76,6 +76,7 @@ public class Controller

private final GroupVersionKind associatedGVK; private final EventProcessor

eventProcessor; private final ControllerHealthInfo controllerHealthInfo; + private final EventSourceContext

eventSourceContext; public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, @@ -98,9 +99,9 @@ public Controller(Reconciler

reconciler, eventProcessor = new EventProcessor<>(eventSourceManager, configurationService); eventSourceManager.postProcessDefaultEventSourcesAfterProcessorInitializer(); controllerHealthInfo = new ControllerHealthInfo(eventSourceManager); - final var context = new EventSourceContext<>( + eventSourceContext = new EventSourceContext<>( eventSourceManager.getControllerResourceEventSource(), configuration, kubernetesClient); - initAndRegisterEventSources(context); + initAndRegisterEventSources(eventSourceContext); configurationService.getMetrics().controllerRegistered(this); } @@ -236,7 +237,8 @@ public void initAndRegisterEventSources(EventSourceContext

context) { } // register created event sources - final var dependentResourcesByName = managedWorkflow.getDependentResourcesByName(); + final var dependentResourcesByName = + managedWorkflow.getDependentResourcesByNameWithoutActivationCondition(); final var size = dependentResourcesByName.size(); if (size > 0) { dependentResourcesByName.forEach((key, dependentResource) -> { @@ -440,4 +442,8 @@ public EventProcessor

getEventProcessor() { public ExecutorServiceManager getExecutorServiceManager() { return getConfiguration().getConfigurationService().getExecutorServiceManager(); } + + public EventSourceContext

eventSourceContext() { + return eventSourceContext; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java index 595341add7..6aab0f1f39 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutor.java @@ -46,12 +46,15 @@ public AbstractWorkflowExecutor(Workflow

workflow, P primary, Context

cont protected synchronized void waitForScheduledExecutionsToRun() { while (true) { try { - this.wait(); + // in case when workflow just contains non-activated dependents, + // it needs to be checked first if there are already no executions + // scheduled at the beginning. if (noMoreExecutionsScheduled()) { break; } else { logger().warn("Notified but still resources under execution. This should not happen."); } + this.wait(); } catch (InterruptedException e) { if (noMoreExecutionsScheduled()) { logger().debug("interrupted, no more executions for: {}", primaryID); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java index e5b89f6c80..13d51b1759 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java @@ -81,6 +81,7 @@ public Workflow

resolve(KubernetesClient client, spec.getReconcileCondition(), spec.getDeletePostCondition(), spec.getReadyCondition(), + spec.getActivationCondition(), resolve(spec, client, configuration)); alreadyResolved.put(node.getName(), node); spec.getDependsOn() diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultWorkflow.java index b2b8716470..d31f42419d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultWorkflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultWorkflow.java @@ -147,4 +147,15 @@ public Map getDependentResourcesByName() { .forEach((name, node) -> resources.put(name, node.getDependentResource())); return resources; } + + public Map getDependentResourcesByNameWithoutActivationCondition() { + final var resources = new HashMap(dependentResourceNodes.size()); + dependentResourceNodes + .forEach((name, node) -> { + if (node.getActivationCondition().isEmpty()) { + resources.put(name, node.getDependentResource()); + } + }); + return resources; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java index 1b12970f48..39a9ba6fe5 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DependentResourceNode.java @@ -16,19 +16,21 @@ public class DependentResourceNode { private Condition reconcilePrecondition; private Condition deletePostcondition; private Condition readyPostcondition; + private Condition activationCondition; private final DependentResource dependentResource; DependentResourceNode(DependentResource dependentResource) { - this(getNameFor(dependentResource), null, null, null, dependentResource); + this(getNameFor(dependentResource), null, null, null, null, dependentResource); } public DependentResourceNode(String name, Condition reconcilePrecondition, Condition deletePostcondition, Condition readyPostcondition, - DependentResource dependentResource) { + Condition activationCondition, DependentResource dependentResource) { this.name = name; this.reconcilePrecondition = reconcilePrecondition; this.deletePostcondition = deletePostcondition; this.readyPostcondition = readyPostcondition; + this.activationCondition = activationCondition; this.dependentResource = dependentResource; } @@ -63,6 +65,10 @@ public Optional> getDeletePostcondition() { return Optional.ofNullable(deletePostcondition); } + public Optional> getActivationCondition() { + return Optional.ofNullable(activationCondition); + } + void setReconcilePrecondition(Condition reconcilePrecondition) { this.reconcilePrecondition = reconcilePrecondition; } @@ -71,6 +77,10 @@ void setDeletePostcondition(Condition cleanupCondition) { this.deletePostcondition = cleanupCondition; } + void setActivationCondition(Condition activationCondition) { + this.activationCondition = activationCondition; + } + public Optional> getReadyPostcondition() { return Optional.ofNullable(readyPostcondition); } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java index c06f17b7d8..839844256e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/Workflow.java @@ -42,4 +42,9 @@ default boolean isEmpty() { default Map getDependentResourcesByName() { return Collections.emptyMap(); } + + @SuppressWarnings("rawtypes") + default Map getDependentResourcesByNameWithoutActivationCondition() { + return Collections.emptyMap(); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java index 0eb176f3aa..a7729859a8 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowBuilder.java @@ -58,6 +58,11 @@ public WorkflowBuilder

withDeletePostcondition(Condition deletePostcondition) return this; } + public WorkflowBuilder

withActivationCondition(Condition activationCondition) { + currentNode.setActivationCondition(activationCondition); + return this; + } + DependentResourceNode getNodeByDependentResource(DependentResource dependentResource) { // first check by name final var node = diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java index 5426eff1aa..74ae47100e 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutor.java @@ -68,11 +68,20 @@ protected void doRun(DependentResourceNode dependentResourceNode, DependentResource dependentResource) { var deletePostCondition = dependentResourceNode.getDeletePostcondition(); - if (dependentResource.isDeletable()) { + var active = + isConditionMet(dependentResourceNode.getActivationCondition(), dependentResource); + + if (dependentResource.isDeletable() && active) { ((Deleter

) dependentResource).delete(primary, context); deleteCalled.add(dependentResourceNode); } - boolean deletePostConditionMet = isConditionMet(deletePostCondition, dependentResource); + + boolean deletePostConditionMet; + if (active) { + deletePostConditionMet = isConditionMet(deletePostCondition, dependentResource); + } else { + deletePostConditionMet = true; + } if (deletePostConditionMet) { markAsVisited(dependentResourceNode); handleDependentCleaned(dependentResourceNode); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java index b1ef16c7b0..27b5ad45da 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutor.java @@ -62,15 +62,40 @@ private synchronized void handleReconcile(DependentResourceNode depend return; } - boolean reconcileConditionMet = isConditionMet(dependentResourceNode.getReconcilePrecondition(), + boolean activationConditionMet = isConditionMet(dependentResourceNode.getActivationCondition(), dependentResourceNode.getDependentResource()); - if (!reconcileConditionMet) { - handleReconcileConditionNotMet(dependentResourceNode); + registerOrDeregisterEventSourceBasedOnActivation(activationConditionMet, dependentResourceNode); + + boolean reconcileConditionMet = true; + if (activationConditionMet) { + reconcileConditionMet = isConditionMet(dependentResourceNode.getReconcilePrecondition(), + dependentResourceNode.getDependentResource()); + } + if (!reconcileConditionMet || !activationConditionMet) { + handleReconcileOrActivationConditionNotMet(dependentResourceNode, activationConditionMet); } else { submit(dependentResourceNode, new NodeReconcileExecutor<>(dependentResourceNode), RECONCILE); } } + private void registerOrDeregisterEventSourceBasedOnActivation(boolean activationConditionMet, + DependentResourceNode dependentResourceNode) { + if (dependentResourceNode.getActivationCondition().isPresent()) { + if (activationConditionMet) { + var eventSource = + dependentResourceNode.getDependentResource().eventSource(context.eventSourceRetriever() + .eventSourceContexForDynamicRegistration()); + var es = eventSource.orElseThrow(); + context.eventSourceRetriever() + .dynamicallyRegisterEventSource(dependentResourceNode.getName(), es); + + } else { + context.eventSourceRetriever() + .dynamicallyDeRegisterEventSource(dependentResourceNode.getName()); + } + } + } + private synchronized void handleDelete(DependentResourceNode dependentResourceNode) { log.debug("Submitting for delete: {}", dependentResourceNode); @@ -83,7 +108,8 @@ private synchronized void handleDelete(DependentResourceNode dependentResourceNo return; } - submit(dependentResourceNode, new NodeDeleteExecutor<>(dependentResourceNode), DELETE); + submit(dependentResourceNode, + new NodeDeleteExecutor<>(dependentResourceNode), DELETE); } private boolean allDependentsDeletedAlready(DependentResourceNode dependentResourceNode) { @@ -141,13 +167,17 @@ protected void doRun(DependentResourceNode dependentResourceNode, DependentResource dependentResource) { var deletePostCondition = dependentResourceNode.getDeletePostcondition(); - // GarbageCollected status is irrelevant here, as this method is only called when a - // precondition does not hold, - // a deleter should be deleted even if it is otherwise garbage collected - if (dependentResource instanceof Deleter) { - ((Deleter

) dependentResource).delete(primary, context); + boolean deletePostConditionMet = true; + if (isConditionMet(dependentResourceNode.getActivationCondition(), dependentResource)) { + // GarbageCollected status is irrelevant here, as this method is only called when a + // precondition does not hold, + // a deleter should be deleted even if it is otherwise garbage collected + if (dependentResource instanceof Deleter) { + ((Deleter

) dependentResource).delete(primary, context); + } + deletePostConditionMet = isConditionMet(deletePostCondition, dependentResource); } - boolean deletePostConditionMet = isConditionMet(deletePostCondition, dependentResource); + if (deletePostConditionMet) { markAsVisited(dependentResourceNode); handleDependentDeleted(dependentResourceNode); @@ -180,20 +210,34 @@ private synchronized void handleDependentsReconcile( } - private void handleReconcileConditionNotMet(DependentResourceNode dependentResourceNode) { + private void handleReconcileOrActivationConditionNotMet( + DependentResourceNode dependentResourceNode, + boolean activationConditionMet) { Set bottomNodes = new HashSet<>(); - markDependentsForDelete(dependentResourceNode, bottomNodes); + markDependentsForDelete(dependentResourceNode, bottomNodes, activationConditionMet); bottomNodes.forEach(this::handleDelete); } private void markDependentsForDelete(DependentResourceNode dependentResourceNode, - Set bottomNodes) { - markedForDelete.add(dependentResourceNode); + Set bottomNodes, boolean activationConditionMet) { + // this is a check so the activation condition is not evaluated twice, + // so if the activation condition was false, this node is not meant to be deleted. var dependents = dependentResourceNode.getParents(); - if (dependents.isEmpty()) { - bottomNodes.add(dependentResourceNode); + if (activationConditionMet) { + markedForDelete.add(dependentResourceNode); + if (dependents.isEmpty()) { + bottomNodes.add(dependentResourceNode); + } else { + dependents.forEach(d -> markDependentsForDelete(d, bottomNodes, true)); + } } else { - dependents.forEach(d -> markDependentsForDelete(d, bottomNodes)); + // this is for an edge case when there is only one resource but that is not active + markAsVisited(dependentResourceNode); + if (dependents.isEmpty()) { + handleNodeExecutionFinish(dependentResourceNode); + } else { + dependents.forEach(d -> markDependentsForDelete(d, bottomNodes, true)); + } } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java index 29785eda01..824c24e276 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceManager.java @@ -17,6 +17,7 @@ import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.ExecutorServiceManager; import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer; import io.javaoperatorsdk.operator.processing.Controller; import io.javaoperatorsdk.operator.processing.LifecycleAware; @@ -231,6 +232,28 @@ public List> getResourceEventSourcesFor(Class d return eventSources.getEventSources(dependentType); } + @Override + public synchronized void dynamicallyRegisterEventSource(String name, EventSource eventSource) { + if (eventSources.existing(name, eventSource) != null) { + return; + } + registerEventSource(name, eventSource); + eventSource.start(); + } + + @Override + public synchronized void dynamicallyDeRegisterEventSource(String name) { + EventSource es = eventSources.remove(name); + if (es != null) { + es.stop(); + } + } + + @Override + public EventSourceContext

eventSourceContexForDynamicRegistration() { + return controller.eventSourceContext(); + } + /** * @deprecated Use {@link #getResourceEventSourceFor(Class)} instead * diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java index a37f35531f..67f149f5cd 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSourceRetriever.java @@ -3,6 +3,8 @@ import java.util.List; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; +import io.javaoperatorsdk.operator.processing.event.source.EventSource; import io.javaoperatorsdk.operator.processing.event.source.ResourceEventSource; public interface EventSourceRetriever

{ @@ -15,4 +17,49 @@ default ResourceEventSource getResourceEventSourceFor(Class depende List> getResourceEventSourcesFor(Class dependentType); + /** + * Registers (and starts) the specified {@link EventSource} dynamically during the reconciliation. + * If an EventSource is already registered with the specified name, the registration will be + * ignored. It is the user's responsibility to handle the naming correctly, thus to not try to + * register different event source with same name that is already registered. + *

+ * This is only needed when your operator needs to adapt dynamically based on optional resources + * that may or may not be present on the target cluster. Even in this situation, it should be + * possible to make these decisions at when the "regular" EventSources are registered so this + * method should not typically be called directly but rather by the framework to support + * activation conditions of dependents, for example. + *

+ *

+ * This method will block until the event source is synced, if needed (as is the case for + * {@link io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource}). + *

+ *

+ * Should multiple reconciliations happen concurrently, only one EventSource with the specified + * name will ever be registered. + *

+ * + * @param name of the event source + * @param eventSource to register + */ + void dynamicallyRegisterEventSource(String name, EventSource eventSource); + + /** + * De-registers (and stops) the {@link EventSource} associated with the specified name. If no such + * source exists, this method will do nothing. + *

+ * This method will block until the event source is de-registered and stopped. If multiple + * reconciliations happen concurrently, all will be blocked until the event source is + * de-registered. + *

+ *

+ * This method is meant only to be used for dynamically registered event sources and should not be + * typically called directly. + *

+ * + * @param name of the event source + */ + void dynamicallyDeRegisterEventSource(String name); + + EventSourceContext

eventSourceContexForDynamicRegistration(); + } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java index 63c94b1b6a..b53b92e122 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/EventSources.java @@ -75,7 +75,7 @@ public void clear() { sources.clear(); } - private NamedEventSource existing(String name, EventSource source) { + public NamedEventSource existing(String name, EventSource source) { final var eventSources = sources.get(keyFor(source)); if (eventSources == null || eventSources.isEmpty()) { return null; @@ -183,4 +183,9 @@ public List> getEventSources(Class dependentTyp .map(es -> (ResourceEventSource) es) .collect(Collectors.toList()); } + + public EventSource remove(String name) { + var optionalMap = sources.values().stream().filter(m -> m.containsKey(name)).findFirst(); + return optionalMap.map(m -> m.remove(name)).orElse(null); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java index f900e602ce..a9bc2d308a 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSource.java @@ -1,7 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; import java.util.*; -import java.util.function.Function; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -10,8 +9,6 @@ import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; -import io.javaoperatorsdk.operator.OperatorException; -import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext; import io.javaoperatorsdk.operator.processing.event.Event; @@ -76,7 +73,6 @@ public class InformerEventSource // we need direct control for the indexer to propagate the just update resource also to the index private final PrimaryToSecondaryIndex primaryToSecondaryIndex; private final PrimaryToSecondaryMapper

primaryToSecondaryMapper; - private Map>> indexerBuffer = new HashMap<>(); private final String id = UUID.randomUUID().toString(); public InformerEventSource( @@ -305,25 +301,6 @@ private boolean acceptedByDeleteFilters(R resource, boolean b) { (genericFilter == null || genericFilter.accept(resource)); } - - // Since this event source instance is created by the user, the ConfigurationService is actually - // injected after it is registered. Some of the subcomponents are initialized at that time here. - @Override - public void setConfigurationService(ConfigurationService configurationService) { - super.setConfigurationService(configurationService); - - super.addIndexers(indexerBuffer); - indexerBuffer = new HashMap<>(); - } - - @Override - public void addIndexers(Map>> indexers) { - if (indexerBuffer == null) { - throw new OperatorException("Cannot add indexers after InformerEventSource started."); - } - indexerBuffer.putAll(indexers); - } - /** * Add an annotation to the resource so that the subsequent will be omitted * diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java index d030e7a8f4..7a1a6ba310 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/source/informer/ManagedInformerEventSource.java @@ -1,9 +1,6 @@ package io.javaoperatorsdk.operator.processing.event.source.informer; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Stream; @@ -16,6 +13,7 @@ import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.informers.ResourceEventHandler; +import io.javaoperatorsdk.operator.OperatorException; import io.javaoperatorsdk.operator.api.config.ConfigurationService; import io.javaoperatorsdk.operator.api.config.NamespaceChangeable; import io.javaoperatorsdk.operator.api.config.ResourceConfiguration; @@ -36,7 +34,11 @@ public abstract class ManagedInformerEventSource, Configurable { private static final Logger log = LoggerFactory.getLogger(ManagedInformerEventSource.class); - private final InformerManager cache; + private InformerManager cache; + private boolean parseResourceVersions; + private ConfigurationService configurationService; + private C configuration; + private Map>> indexers = new HashMap<>(); protected TemporaryResourceCache temporaryResourceCache; protected MixedOperation, Resource> client; @@ -44,9 +46,9 @@ protected ManagedInformerEventSource( MixedOperation, Resource> client, C configuration, boolean parseResourceVersions) { super(configuration.getResourceClass()); + this.parseResourceVersions = parseResourceVersions; this.client = client; - temporaryResourceCache = new TemporaryResourceCache<>(this, parseResourceVersions); - this.cache = new InformerManager<>(client, configuration, this); + this.configuration = configuration; } @Override @@ -77,6 +79,10 @@ public void changeNamespaces(Set namespaces) { @Override public void start() { + temporaryResourceCache = new TemporaryResourceCache<>(this, parseResourceVersions); + this.cache = new InformerManager<>(client, configuration, this); + cache.setConfigurationService(configurationService); + cache.addIndexers(indexers); manager().start(); super.start(); } @@ -129,7 +135,10 @@ void setTemporalResourceCache(TemporaryResourceCache temporaryResourceCache) @Override public void addIndexers(Map>> indexers) { - cache.addIndexers(indexers); + if (isRunning()) { + throw new OperatorException("Cannot add indexers after InformerEventSource started."); + } + this.indexers.putAll(indexers); } @Override @@ -164,7 +173,7 @@ public ResourceConfiguration getInformerConfiguration() { @Override public C configuration() { - return manager().configuration(); + return configuration; } @Override @@ -175,6 +184,6 @@ public String toString() { } public void setConfigurationService(ConfigurationService configurationService) { - cache.setConfigurationService(configurationService); + this.configurationService = configurationService; } } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutorTest.java index e8b26184c4..6cfdb3dc63 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/AbstractWorkflowExecutorTest.java @@ -25,9 +25,9 @@ public class AbstractWorkflowExecutorTest { protected GarbageCollectedDeleter gcDeleter = new GarbageCollectedDeleter("GC_DELETER"); @SuppressWarnings("rawtypes") - protected final Condition noMetDeletePostCondition = (primary, secondary, context) -> false; + protected final Condition notMetCondition = (primary, secondary, context) -> false; @SuppressWarnings("rawtypes") - protected final Condition metDeletePostCondition = (primary, secondary, context) -> true; + protected final Condition metCondition = (primary, secondary, context) -> true; protected List executionHistory = Collections.synchronizedList(new ArrayList<>()); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java index 25c0ad139b..b314b5b112 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java @@ -21,7 +21,7 @@ public class ManagedWorkflowTestUtils { @SuppressWarnings("unchecked") public static DependentResourceSpec createDRS(String name, String... dependOns) { return new DependentResourceSpec(EmptyTestDependentResource.class, name, Set.of(dependOns), - null, null, null, null); + null, null, null, null, null); } public static DependentResourceSpec createDRSWithTraits(String name, diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutorTest.java index 64fdc6e6f7..3b34dcf458 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupExecutorTest.java @@ -21,6 +21,7 @@ class WorkflowCleanupExecutorTest extends AbstractWorkflowExecutorTest { protected TestDeleterDependent dd1 = new TestDeleterDependent("DR_DELETER_1"); protected TestDeleterDependent dd2 = new TestDeleterDependent("DR_DELETER_2"); protected TestDeleterDependent dd3 = new TestDeleterDependent("DR_DELETER_3"); + protected TestDeleterDependent dd4 = new TestDeleterDependent("DR_DELETER_4"); @SuppressWarnings("unchecked") Context mockContext = mock(Context.class); @@ -76,7 +77,7 @@ void dontDeleteIfDependentErrored() { void cleanupConditionTrivialCase() { var workflow = new WorkflowBuilder() .addDependentResource(dd1) - .addDependentResource(dd2).dependsOn(dd1).withDeletePostcondition(noMetDeletePostCondition) + .addDependentResource(dd2).dependsOn(dd1).withDeletePostcondition(notMetCondition) .build(); var res = workflow.cleanup(new TestCustomResource(), mockContext); @@ -91,7 +92,7 @@ void cleanupConditionTrivialCase() { void cleanupConditionMet() { var workflow = new WorkflowBuilder() .addDependentResource(dd1) - .addDependentResource(dd2).dependsOn(dd1).withDeletePostcondition(metDeletePostCondition) + .addDependentResource(dd2).dependsOn(dd1).withDeletePostcondition(metCondition) .build(); var res = workflow.cleanup(new TestCustomResource(), mockContext); @@ -105,12 +106,10 @@ void cleanupConditionMet() { @Test void cleanupConditionDiamondWorkflow() { - TestDeleterDependent dd4 = new TestDeleterDependent("DR_DELETER_4"); - var workflow = new WorkflowBuilder() .addDependentResource(dd1) .addDependentResource(dd2).dependsOn(dd1) - .addDependentResource(dd3).dependsOn(dd1).withDeletePostcondition(noMetDeletePostCondition) + .addDependentResource(dd3).dependsOn(dd1).withDeletePostcondition(notMetCondition) .addDependentResource(dd4).dependsOn(dd2, dd3) .build(); @@ -141,4 +140,98 @@ void dontDeleteIfGarbageCollected() { Assertions.assertThat(res.getDeleteCalledOnDependents()).isEmpty(); } + @Test + void ifDependentActiveDependentNormallyDeleted() { + var workflow = new WorkflowBuilder() + .addDependentResource(dd1) + .addDependentResource(dd2).dependsOn(dd1) + .addDependentResource(dd3).dependsOn(dd1) + .withActivationCondition(metCondition) + .addDependentResource(dd4).dependsOn(dd2, dd3) + .build(); + + var res = workflow.cleanup(new TestCustomResource(), mockContext); + + assertThat(executionHistory) + .reconciledInOrder(dd4, dd2, dd1) + .reconciledInOrder(dd4, dd3, dd1); + + Assertions.assertThat(res.getDeleteCalledOnDependents()).containsExactlyInAnyOrder(dd4, dd3, + dd2, dd1); + } + + @Test + void ifDependentActiveDeletePostConditionIsChecked() { + var workflow = new WorkflowBuilder() + .addDependentResource(dd1) + .addDependentResource(dd2).dependsOn(dd1) + .addDependentResource(dd3).dependsOn(dd1) + .withDeletePostcondition(notMetCondition) + .withActivationCondition(metCondition) + .addDependentResource(dd4).dependsOn(dd2, dd3) + .build(); + + var res = workflow.cleanup(new TestCustomResource(), mockContext); + + assertThat(executionHistory) + .reconciledInOrder(dd4, dd2) + .reconciledInOrder(dd4, dd3) + .notReconciled(dr1); + + Assertions.assertThat(res.getDeleteCalledOnDependents()).containsExactlyInAnyOrder(dd4, dd3, + dd2); + Assertions.assertThat(res.getErroredDependents()).isEmpty(); + Assertions.assertThat(res.getPostConditionNotMetDependents()).containsExactlyInAnyOrder(dd3); + } + + @Test + void ifDependentInactiveDeleteIsNotCalled() { + var workflow = new WorkflowBuilder() + .addDependentResource(dd1) + .addDependentResource(dd2).dependsOn(dd1) + .addDependentResource(dd3).dependsOn(dd1) + .withActivationCondition(notMetCondition) + .addDependentResource(dd4).dependsOn(dd2, dd3) + .build(); + + var res = workflow.cleanup(new TestCustomResource(), mockContext); + + assertThat(executionHistory) + .reconciledInOrder(dd4, dd2, dd1); + + Assertions.assertThat(res.getDeleteCalledOnDependents()).containsExactlyInAnyOrder(dd4, + dd2, dd1); + } + + @Test + void ifDependentInactiveDeletePostConditionNotChecked() { + var workflow = new WorkflowBuilder() + .addDependentResource(dd1) + .addDependentResource(dd2).dependsOn(dd1) + .addDependentResource(dd3).dependsOn(dd1) + .withDeletePostcondition(notMetCondition) + .withActivationCondition(notMetCondition) + .addDependentResource(dd4).dependsOn(dd2, dd3) + .build(); + + var res = workflow.cleanup(new TestCustomResource(), mockContext); + + assertThat(executionHistory) + .reconciledInOrder(dd4, dd2, dd1); + + Assertions.assertThat(res.getPostConditionNotMetDependents()).isEmpty(); + } + + @Test + void singleInactiveDependent() { + var workflow = new WorkflowBuilder() + .addDependentResource(dd1) + .withActivationCondition(notMetCondition) + .build(); + + workflow.cleanup(new TestCustomResource(), mockContext); + + assertThat(executionHistory).notReconciled(dd1); + } + } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutorTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutorTest.java index b3c4ea35e9..099d5fad2b 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutorTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileExecutorTest.java @@ -9,33 +9,27 @@ import io.javaoperatorsdk.operator.AggregatedOperatorException; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.event.EventSourceRetriever; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static io.javaoperatorsdk.operator.processing.dependent.workflow.ExecutionAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @SuppressWarnings("rawtypes") class WorkflowReconcileExecutorTest extends AbstractWorkflowExecutorTest { - - - private final Condition met_reconcile_condition = (primary, secondary, context) -> true; - private final Condition not_met_reconcile_condition = (primary, secondary, context) -> false; - - private final Condition metReadyCondition = - (primary, secondary, context) -> true; - private final Condition notMetReadyCondition = - (primary, secondary, context) -> false; - @SuppressWarnings("unchecked") Context mockContext = mock(Context.class); ExecutorService executorService = Executors.newCachedThreadPool(); + TestDependent dr3 = new TestDependent("DR_3"); + TestDependent dr4 = new TestDependent("DR_4"); + @BeforeEach void setup() { when(mockContext.getWorkflowExecutorService()).thenReturn(executorService); + when(mockContext.eventSourceRetriever()).thenReturn(mock(EventSourceRetriever.class)); } @Test @@ -70,7 +64,6 @@ void reconciliationWithSimpleDependsOn() { @Test void reconciliationWithTwoTheDependsOns() { - TestDependent dr3 = new TestDependent("DR_3"); var workflow = new WorkflowBuilder() .addDependentResource(dr1) @@ -90,9 +83,6 @@ void reconciliationWithTwoTheDependsOns() { @Test void diamondShareWorkflowReconcile() { - TestDependent dr3 = new TestDependent("DR_3"); - TestDependent dr4 = new TestDependent("DR_4"); - var workflow = new WorkflowBuilder() .addDependentResource(dr1) .addDependentResource(dr2).dependsOn(dr1) @@ -152,7 +142,6 @@ void dependentsOnErroredResourceNotReconciled() { @Test void oneBranchErrorsOtherCompletes() { - TestDependent dr3 = new TestDependent("DR_3"); var workflow = new WorkflowBuilder() .addDependentResource(dr1) @@ -194,9 +183,9 @@ void onlyOneDependsOnErroredResourceNotReconciled() { @Test void simpleReconcileCondition() { var workflow = new WorkflowBuilder() - .addDependentResource(dr1).withReconcilePrecondition(not_met_reconcile_condition) - .addDependentResource(dr2).withReconcilePrecondition(met_reconcile_condition) - .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .addDependentResource(dr1).withReconcilePrecondition(notMetCondition) + .addDependentResource(dr2).withReconcilePrecondition(metCondition) + .addDependentResource(drDeleter).withReconcilePrecondition(notMetCondition) .build(); var res = workflow.reconcile(new TestCustomResource(), mockContext); @@ -213,7 +202,7 @@ void triangleOnceConditionNotMet() { var workflow = new WorkflowBuilder() .addDependentResource(dr1) .addDependentResource(dr2).dependsOn(dr1) - .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .addDependentResource(drDeleter).withReconcilePrecondition(notMetCondition) .dependsOn(dr1) .build(); @@ -232,11 +221,11 @@ void reconcileConditionTransitiveDelete() { var workflow = new WorkflowBuilder() .addDependentResource(dr1) .addDependentResource(dr2).dependsOn(dr1) - .withReconcilePrecondition(not_met_reconcile_condition) + .withReconcilePrecondition(notMetCondition) .addDependentResource(drDeleter).dependsOn(dr2) - .withReconcilePrecondition(met_reconcile_condition) + .withReconcilePrecondition(metCondition) .addDependentResource(drDeleter2).dependsOn(drDeleter) - .withReconcilePrecondition(met_reconcile_condition) + .withReconcilePrecondition(metCondition) .build(); var res = workflow.reconcile(new TestCustomResource(), mockContext); @@ -257,9 +246,9 @@ void reconcileConditionAlsoErrorDependsOn() { var workflow = new WorkflowBuilder() .addDependentResource(drError) - .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .addDependentResource(drDeleter).withReconcilePrecondition(notMetCondition) .addDependentResource(drDeleter2).dependsOn(drError, drDeleter) - .withReconcilePrecondition(met_reconcile_condition) + .withReconcilePrecondition(metCondition) .withThrowExceptionFurther(false) .build(); @@ -280,7 +269,7 @@ void reconcileConditionAlsoErrorDependsOn() { void oneDependsOnConditionNotMet() { var workflow = new WorkflowBuilder() .addDependentResource(dr1) - .addDependentResource(dr2).withReconcilePrecondition(not_met_reconcile_condition) + .addDependentResource(dr2).withReconcilePrecondition(notMetCondition) .addDependentResource(drDeleter).dependsOn(dr1, dr2) .build(); @@ -300,7 +289,7 @@ void deletedIfReconcileConditionNotMet() { var workflow = new WorkflowBuilder() .addDependentResource(dr1) .addDependentResource(drDeleter).dependsOn(dr1) - .withReconcilePrecondition(not_met_reconcile_condition) + .withReconcilePrecondition(notMetCondition) .addDependentResource(drDeleter2).dependsOn(dr1, drDeleter) .build(); @@ -323,7 +312,7 @@ void deleteDoneInReverseOrder() { var workflow = new WorkflowBuilder() .addDependentResource(dr1) - .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .addDependentResource(drDeleter).withReconcilePrecondition(notMetCondition) .dependsOn(dr1) .addDependentResource(drDeleter2).dependsOn(drDeleter) .addDependentResource(drDeleter3).dependsOn(drDeleter) @@ -349,10 +338,10 @@ void diamondDeleteWithPostConditionInMiddle() { TestDeleterDependent drDeleter4 = new TestDeleterDependent("DR_DELETER_4"); var workflow = new WorkflowBuilder() - .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .addDependentResource(drDeleter).withReconcilePrecondition(notMetCondition) .addDependentResource(drDeleter2).dependsOn(drDeleter) .addDependentResource(drDeleter3).dependsOn(drDeleter) - .withDeletePostcondition(noMetDeletePostCondition) + .withDeletePostcondition(this.notMetCondition) .addDependentResource(drDeleter4).dependsOn(drDeleter3, drDeleter2) .build(); @@ -373,7 +362,7 @@ void diamondDeleteErrorInMiddle() { TestDeleterDependent drDeleter3 = new TestDeleterDependent("DR_DELETER_3"); var workflow = new WorkflowBuilder() - .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .addDependentResource(drDeleter).withReconcilePrecondition(notMetCondition) .addDependentResource(drDeleter2).dependsOn(drDeleter) .addDependentResource(errorDD).dependsOn(drDeleter) .addDependentResource(drDeleter3).dependsOn(errorDD, drDeleter2) @@ -394,7 +383,7 @@ void diamondDeleteErrorInMiddle() { @Test void readyConditionTrivialCase() { var workflow = new WorkflowBuilder() - .addDependentResource(dr1).withReadyPostcondition(metReadyCondition) + .addDependentResource(dr1).withReadyPostcondition(metCondition) .addDependentResource(dr2).dependsOn(dr1) .build(); @@ -410,7 +399,7 @@ void readyConditionTrivialCase() { @Test void readyConditionNotMetTrivialCase() { var workflow = new WorkflowBuilder() - .addDependentResource(dr1).withReadyPostcondition(notMetReadyCondition) + .addDependentResource(dr1).withReadyPostcondition(notMetCondition) .addDependentResource(dr2).dependsOn(dr1) .build(); @@ -426,10 +415,9 @@ void readyConditionNotMetTrivialCase() { @Test void readyConditionNotMetInOneParent() { - TestDependent dr3 = new TestDependent("DR_3"); var workflow = new WorkflowBuilder() - .addDependentResource(dr1).withReadyPostcondition(notMetReadyCondition) + .addDependentResource(dr1).withReadyPostcondition(notMetCondition) .addDependentResource(dr2) .addDependentResource(dr3).dependsOn(dr1, dr2) .build(); @@ -444,12 +432,9 @@ void readyConditionNotMetInOneParent() { @Test void diamondShareWithReadyCondition() { - TestDependent dr3 = new TestDependent("DR_3"); - TestDependent dr4 = new TestDependent("DR_4"); - var workflow = new WorkflowBuilder() .addDependentResource(dr1) - .addDependentResource(dr2).dependsOn(dr1).withReadyPostcondition(notMetReadyCondition) + .addDependentResource(dr2).dependsOn(dr1).withReadyPostcondition(notMetCondition) .addDependentResource(dr3).dependsOn(dr1) .addDependentResource(dr4).dependsOn(dr2, dr3) .build(); @@ -469,7 +454,7 @@ void diamondShareWithReadyCondition() { @Test void garbageCollectedResourceIsDeletedIfReconcilePreconditionDoesNotHold() { var workflow = new WorkflowBuilder() - .addDependentResource(gcDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .addDependentResource(gcDeleter).withReconcilePrecondition(notMetCondition) .build(); var res = workflow.reconcile(new TestCustomResource(), mockContext); @@ -481,7 +466,7 @@ void garbageCollectedResourceIsDeletedIfReconcilePreconditionDoesNotHold() { @Test void garbageCollectedDeepResourceIsDeletedIfReconcilePreconditionDoesNotHold() { var workflow = new WorkflowBuilder() - .addDependentResource(dr1).withReconcilePrecondition(not_met_reconcile_condition) + .addDependentResource(dr1).withReconcilePrecondition(notMetCondition) .addDependentResource(gcDeleter).dependsOn(dr1) .build(); @@ -491,4 +476,101 @@ void garbageCollectedDeepResourceIsDeletedIfReconcilePreconditionDoesNotHold() { assertThat(executionHistory).deleted(gcDeleter); } + @Test + void notReconciledIfActivationConditionNotMet() { + var workflow = new WorkflowBuilder() + .addDependentResource(dr1) + .withActivationCondition(notMetCondition) + .addDependentResource(dr2) + .build(); + var res = workflow.reconcile(new TestCustomResource(), mockContext); + + assertThat(executionHistory).reconciled(dr2).notReconciled(dr1); + Assertions.assertThat(res.getErroredDependents()).isEmpty(); + Assertions.assertThat(res.getReconciledDependents()).contains(dr2); + } + + @Test + void dependentsOnANonActiveDependentNotReconciled() { + var workflow = new WorkflowBuilder() + .addDependentResource(dr1) + .withActivationCondition(notMetCondition) + .addDependentResource(dr2) + .addDependentResource(dr3).dependsOn(dr1) + .build(); + var res = workflow.reconcile(new TestCustomResource(), mockContext); + + assertThat(executionHistory).reconciled(dr2).notReconciled(dr1, dr3); + Assertions.assertThat(res.getErroredDependents()).isEmpty(); + Assertions.assertThat(res.getReconciledDependents()).contains(dr2); + } + + @Test + void readyConditionNotCheckedOnNonActiveDependent() { + var workflow = new WorkflowBuilder() + .addDependentResource(dr1) + .withActivationCondition(notMetCondition) + .withReadyPostcondition(notMetCondition) + .addDependentResource(dr2) + .addDependentResource(dr3).dependsOn(dr1) + .build(); + + var res = workflow.reconcile(new TestCustomResource(), mockContext); + + Assertions.assertThat(res.getNotReadyDependents()).isEmpty(); + } + + @Test + void reconcilePreconditionNotCheckedOnNonActiveDependent() { + var precondition = mock(Condition.class); + + var workflow = new WorkflowBuilder() + .addDependentResource(dr1) + .withActivationCondition(notMetCondition) + .withReconcilePrecondition(precondition) + .build(); + + workflow.reconcile(new TestCustomResource(), mockContext); + + verify(precondition, never()).isMet(any(), any(), any()); + } + + @Test + void deletesDependentsOfNonActiveDependentButNotTheNonActive() { + TestDeleterDependent drDeleter2 = new TestDeleterDependent("DR_DELETER_2"); + TestDeleterDependent drDeleter3 = new TestDeleterDependent("DR_DELETER_2"); + + var workflow = new WorkflowBuilder() + .addDependentResource(dr1).withActivationCondition(notMetCondition) + .addDependentResource(drDeleter).dependsOn(dr1) + .addDependentResource(drDeleter2).dependsOn(drDeleter) + .withActivationCondition(notMetCondition) + .addDependentResource(drDeleter3).dependsOn(drDeleter2) + .build(); + + var res = workflow.reconcile(new TestCustomResource(), mockContext); + + Assertions.assertThat(res.getReconciledDependents()).isEmpty(); + assertThat(executionHistory).deleted(drDeleter, drDeleter3) + .notReconciled(dr1, + drDeleter2); + } + + @Test + void activationConditionOnlyCalledOnceOnDeleteDependents() { + TestDeleterDependent drDeleter2 = new TestDeleterDependent("DR_DELETER_2"); + var condition = mock(Condition.class); + when(condition.isMet(any(), any(), any())).thenReturn(false); + + var workflow = new WorkflowBuilder() + .addDependentResource(drDeleter).withActivationCondition(condition) + .addDependentResource(drDeleter2).dependsOn(drDeleter) + .build(); + + workflow.reconcile(new TestCustomResource(), mockContext); + + assertThat(executionHistory).deleted(drDeleter2); + verify(condition, times(1)).isMet(any(), any(), any()); + } + } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java index 87ec3f9eba..64f0993139 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/controller/ControllerResourceEventSourceTest.java @@ -37,7 +37,7 @@ class ControllerResourceEventSourceTest extends @BeforeEach public void setup() { - setUpSource(new ControllerResourceEventSource<>(testController), false, + setUpSource(new ControllerResourceEventSource<>(testController), true, new BaseConfigurationService()); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java index 0881cccaf2..ce3c52076d 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/event/source/informer/InformerEventSourceTest.java @@ -55,15 +55,16 @@ void setup() { when(informerConfiguration.getResourceClass()).thenReturn(Deployment.class); informerEventSource = new InformerEventSource<>(informerConfiguration, clientMock); - informerEventSource.setTemporalResourceCache(temporaryResourceCacheMock); - informerEventSource.setEventHandler(eventHandlerMock); - + informerEventSource.setEventHandler(eventHandlerMock); + informerEventSource.setConfigurationService(new BaseConfigurationService()); SecondaryToPrimaryMapper secondaryToPrimaryMapper = mock(SecondaryToPrimaryMapper.class); when(informerConfiguration.getSecondaryToPrimaryMapper()) .thenReturn(secondaryToPrimaryMapper); when(secondaryToPrimaryMapper.toPrimaryResourceIDs(any())) .thenReturn(Set.of(ResourceID.fromResource(testDeployment()))); + informerEventSource.start(); + informerEventSource.setTemporalResourceCache(temporaryResourceCacheMock); } @Test diff --git a/operator-framework/pom.xml b/operator-framework/pom.xml index d5b8a5371b..943acc11d2 100644 --- a/operator-framework/pom.xml +++ b/operator-framework/pom.xml @@ -64,6 +64,12 @@ crd-generator-apt test + + + io.fabric8 + openshift-client-api + test + org.apache.logging.log4j log4j-slf4j-impl diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowActivationConditionIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowActivationConditionIT.java new file mode 100644 index 0000000000..38a11e0438 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowActivationConditionIT.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.workflowactivationcondition.WorkflowActivationConditionCustomResource; +import io.javaoperatorsdk.operator.sample.workflowactivationcondition.WorkflowActivationConditionReconciler; +import io.javaoperatorsdk.operator.sample.workflowactivationcondition.WorkflowActivationConditionSpec; + +import static io.javaoperatorsdk.operator.sample.workflowactivationcondition.ConfigMapDependentResource.DATA_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class WorkflowActivationConditionIT { + + public static final String TEST_RESOURCE_NAME = "test1"; + public static final String TEST_DATA = "test data"; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(WorkflowActivationConditionReconciler.class) + .build(); + + // Without activation condition this would fail / there would be errors. + @Test + void reconciledOnVanillaKubernetesDespiteRouteInWorkflow() { + extension.create(testResource()); + + await().untilAsserted(() -> { + var cm = extension.get(ConfigMap.class, TEST_RESOURCE_NAME); + assertThat(cm).isNotNull(); + assertThat(cm.getData()).containsEntry(DATA_KEY, TEST_DATA); + }); + } + + private WorkflowActivationConditionCustomResource testResource() { + var res = new WorkflowActivationConditionCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(TEST_RESOURCE_NAME) + .build()); + res.setSpec(new WorkflowActivationConditionSpec()); + res.getSpec().setValue(TEST_DATA); + return res; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowMultipleActivationIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowMultipleActivationIT.java new file mode 100644 index 0000000000..a7f93935ee --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowMultipleActivationIT.java @@ -0,0 +1,137 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; + +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.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.workflowmultipleactivation.*; + +import static io.javaoperatorsdk.operator.sample.workflowactivationcondition.ConfigMapDependentResource.DATA_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class WorkflowMultipleActivationIT { + + public static final String INITIAL_DATA = "initial data"; + public static final String TEST_RESOURCE1 = "test1"; + public static final String TEST_RESOURCE2 = "test2"; + public static final String CHANGED_VALUE = "changed value"; + public static final int POLL_DELAY = 300; + + @RegisterExtension + LocallyRunOperatorExtension extension = + LocallyRunOperatorExtension.builder() + .withReconciler(WorkflowMultipleActivationReconciler.class) + .build(); + + @Test + void deactivatingAndReactivatingDependent() { + ActivationCondition.MET = true; + var cr1 = extension.create(testResource()); + + await().untilAsserted(() -> { + var cm = extension.get(ConfigMap.class, TEST_RESOURCE1); + var secret = extension.get(Secret.class, TEST_RESOURCE1); + assertThat(cm).isNotNull(); + assertThat(secret).isNotNull(); + assertThat(cm.getData()).containsEntry(DATA_KEY, INITIAL_DATA); + }); + + extension.delete(cr1); + + await().untilAsserted(() -> { + var cm = extension.get(ConfigMap.class, TEST_RESOURCE1); + assertThat(cm).isNull(); + }); + + ActivationCondition.MET = false; + cr1 = extension.create(testResource()); + + await().untilAsserted(() -> { + var cm = extension.get(ConfigMap.class, TEST_RESOURCE1); + var secret = extension.get(Secret.class, TEST_RESOURCE1); + assertThat(cm).isNull(); + assertThat(secret).isNotNull(); + }); + + ActivationCondition.MET = true; + cr1.getSpec().setValue(CHANGED_VALUE); + extension.replace(cr1); + + await().untilAsserted(() -> { + var cm = extension.get(ConfigMap.class, TEST_RESOURCE1); + assertThat(cm).isNotNull(); + assertThat(cm.getData()).containsEntry(DATA_KEY, CHANGED_VALUE); + }); + + ActivationCondition.MET = false; + cr1.getSpec().setValue(INITIAL_DATA); + extension.replace(cr1); + + await().pollDelay(Duration.ofMillis(POLL_DELAY)).untilAsserted(() -> { + var cm = extension.get(ConfigMap.class, TEST_RESOURCE1); + assertThat(cm).isNotNull(); + // data not changed + assertThat(cm.getData()).containsEntry(DATA_KEY, CHANGED_VALUE); + }); + + var numOfReconciliation = + extension.getReconcilerOfType(WorkflowMultipleActivationReconciler.class) + .getNumberOfReconciliationExecution(); + var actualCM = extension.get(ConfigMap.class, TEST_RESOURCE1); + actualCM.getData().put("data2", "additionaldata"); + extension.replace(actualCM); + await().pollDelay(Duration.ofMillis(POLL_DELAY)).untilAsserted(() -> { + // change in config map does not induce reconciliation if inactive (thus informer is not + // present) + assertThat(extension.getReconcilerOfType(WorkflowMultipleActivationReconciler.class) + .getNumberOfReconciliationExecution()).isEqualTo(numOfReconciliation); + }); + + extension.delete(cr1); + await().pollDelay(Duration.ofMillis(POLL_DELAY)).untilAsserted(() -> { + var cm = extension.get(ConfigMap.class, TEST_RESOURCE1); + assertThat(cm).isNotNull(); + }); + } + + WorkflowMultipleActivationCustomResource testResource(String name) { + var res = new WorkflowMultipleActivationCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(name) + .build()); + res.setSpec(new WorkflowMultipleActivationSpec()); + res.getSpec().setValue(INITIAL_DATA); + return res; + } + + WorkflowMultipleActivationCustomResource testResource() { + return testResource(TEST_RESOURCE1); + } + + WorkflowMultipleActivationCustomResource testResource2() { + return testResource(TEST_RESOURCE2); + } + + @Test + void simpleConcurrencyTest() { + ActivationCondition.MET = true; + extension.create(testResource()); + extension.create(testResource2()); + + await().untilAsserted(() -> { + var cm = extension.get(ConfigMap.class, TEST_RESOURCE1); + var cm2 = extension.get(ConfigMap.class, TEST_RESOURCE2); + assertThat(cm).isNotNull(); + assertThat(cm2).isNotNull(); + assertThat(cm.getData()).containsEntry(DATA_KEY, INITIAL_DATA); + assertThat(cm2.getData()).containsEntry(DATA_KEY, INITIAL_DATA); + }); + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/ConfigMapDependentResource.java new file mode 100644 index 0000000000..7433c31c73 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/ConfigMapDependentResource.java @@ -0,0 +1,30 @@ +package io.javaoperatorsdk.operator.sample.workflowactivationcondition; + +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; + +public class ConfigMapDependentResource + extends CRUDKubernetesDependentResource { + + public static final String DATA_KEY = "data"; + + public ConfigMapDependentResource() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(WorkflowActivationConditionCustomResource primary, + Context context) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + configMap.setData(Map.of(DATA_KEY, primary.getSpec().getValue())); + return configMap; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/IsOpenShiftCondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/IsOpenShiftCondition.java new file mode 100644 index 0000000000..79434409b8 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/IsOpenShiftCondition.java @@ -0,0 +1,18 @@ +package io.javaoperatorsdk.operator.sample.workflowactivationcondition; + +import io.fabric8.openshift.api.model.Route; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; + +public class IsOpenShiftCondition + implements Condition { + @Override + public boolean isMet( + DependentResource dependentResource, + WorkflowActivationConditionCustomResource primary, + Context context) { + // we are testing if the reconciliation still works on Kubernetes, so this always false; + return false; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/RouteDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/RouteDependentResource.java new file mode 100644 index 0000000000..de5d52f76a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/RouteDependentResource.java @@ -0,0 +1,27 @@ +package io.javaoperatorsdk.operator.sample.workflowactivationcondition; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.openshift.api.model.Route; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; + +public class RouteDependentResource + extends CRUDKubernetesDependentResource { + + public RouteDependentResource() { + super(Route.class); + } + + @Override + protected Route desired(WorkflowActivationConditionCustomResource primary, + Context context) { + // basically does not matter since this should not be called + Route route = new Route(); + route.setMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + + return route; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/WorkflowActivationConditionCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/WorkflowActivationConditionCustomResource.java new file mode 100644 index 0000000000..63d4bc343c --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/WorkflowActivationConditionCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.workflowactivationcondition; + +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("wac") +public class WorkflowActivationConditionCustomResource + extends CustomResource + implements Namespaced { + + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/WorkflowActivationConditionReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/WorkflowActivationConditionReconciler.java new file mode 100644 index 0000000000..33db3043ba --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/WorkflowActivationConditionReconciler.java @@ -0,0 +1,21 @@ +package io.javaoperatorsdk.operator.sample.workflowactivationcondition; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +@ControllerConfiguration(dependents = { + @Dependent(type = ConfigMapDependentResource.class), + @Dependent(type = RouteDependentResource.class, + activationCondition = IsOpenShiftCondition.class) +}) +public class WorkflowActivationConditionReconciler + implements Reconciler { + + @Override + public UpdateControl reconcile( + WorkflowActivationConditionCustomResource resource, + Context context) { + + return UpdateControl.noUpdate(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/WorkflowActivationConditionSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/WorkflowActivationConditionSpec.java new file mode 100644 index 0000000000..826fe04958 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowactivationcondition/WorkflowActivationConditionSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.sample.workflowactivationcondition; + +public class WorkflowActivationConditionSpec { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/ActivationCondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/ActivationCondition.java new file mode 100644 index 0000000000..c8be8467df --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/ActivationCondition.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.sample.workflowmultipleactivation; + +import io.fabric8.openshift.api.model.Route; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; + +public class ActivationCondition + implements Condition { + + public static volatile boolean MET = true; + + @Override + public boolean isMet( + DependentResource dependentResource, + WorkflowMultipleActivationCustomResource primary, + Context context) { + return MET; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/ConfigMapDependentResource.java new file mode 100644 index 0000000000..e245cb222d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/ConfigMapDependentResource.java @@ -0,0 +1,31 @@ +package io.javaoperatorsdk.operator.sample.workflowmultipleactivation; + +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDNoGCKubernetesDependentResource; + +public class ConfigMapDependentResource + extends + CRUDNoGCKubernetesDependentResource { + + public static final String DATA_KEY = "data"; + + public ConfigMapDependentResource() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(WorkflowMultipleActivationCustomResource primary, + Context context) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + configMap.setData(Map.of(DATA_KEY, primary.getSpec().getValue())); + return configMap; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/SecretDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/SecretDependentResource.java new file mode 100644 index 0000000000..decd5a346d --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/SecretDependentResource.java @@ -0,0 +1,31 @@ +package io.javaoperatorsdk.operator.sample.workflowmultipleactivation; + +import java.util.Base64; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.api.model.Secret; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; + +public class SecretDependentResource + extends CRUDKubernetesDependentResource { + + public SecretDependentResource() { + super(Secret.class); + } + + @Override + protected Secret desired(WorkflowMultipleActivationCustomResource primary, + Context context) { + // basically does not matter since this should not be called + Secret secret = new Secret(); + secret.setMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + secret.setData(Map.of("data", + Base64.getEncoder().encodeToString(primary.getSpec().getValue().getBytes()))); + return secret; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/WorkflowMultipleActivationCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/WorkflowMultipleActivationCustomResource.java new file mode 100644 index 0000000000..9c642e9635 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/WorkflowMultipleActivationCustomResource.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.workflowmultipleactivation; + +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("mwac") +public class WorkflowMultipleActivationCustomResource + extends CustomResource + implements Namespaced { + + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/WorkflowMultipleActivationReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/WorkflowMultipleActivationReconciler.java new file mode 100644 index 0000000000..8277e7f8e7 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/WorkflowMultipleActivationReconciler.java @@ -0,0 +1,34 @@ +package io.javaoperatorsdk.operator.sample.workflowmultipleactivation; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; +import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +@ControllerConfiguration(dependents = { + @Dependent(type = ConfigMapDependentResource.class, + activationCondition = ActivationCondition.class), + @Dependent(type = SecretDependentResource.class) +}) +public class WorkflowMultipleActivationReconciler + implements Reconciler { + + private final AtomicInteger numberOfReconciliationExecution = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + WorkflowMultipleActivationCustomResource resource, + Context context) { + + numberOfReconciliationExecution.incrementAndGet(); + + return UpdateControl.noUpdate(); + } + + public int getNumberOfReconciliationExecution() { + return numberOfReconciliationExecution.get(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/WorkflowMultipleActivationSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/WorkflowMultipleActivationSpec.java new file mode 100644 index 0000000000..b342bb331f --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowmultipleactivation/WorkflowMultipleActivationSpec.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.sample.workflowmultipleactivation; + +public class WorkflowMultipleActivationSpec { + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +}