diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java index c86c3f95b6..ba0c5083be 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/AnnotationControllerConfiguration.java @@ -1,5 +1,6 @@ package io.javaoperatorsdk.operator.api.config; +import java.lang.reflect.InvocationTargetException; import java.time.Duration; import java.util.Arrays; import java.util.Collections; @@ -19,9 +20,11 @@ import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.VoidCondition; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter; import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilters; @@ -166,12 +169,15 @@ public List getDependentResources() { } final var name = getName(dependent, dependentType); - final var spec = specsMap.get(name); + var spec = specsMap.get(name); if (spec != null) { throw new IllegalArgumentException( "A DependentResource named: " + name + " already exists: " + spec); } - specsMap.put(name, new DependentResourceSpec(dependentType, config, name)); + spec = new DependentResourceSpec(dependentType, config, name); + spec.setDependsOn(Set.of(dependent.dependsOn())); + addConditions(spec, dependent); + specsMap.put(name, spec); } specs = specsMap.values().stream().collect(Collectors.toUnmodifiableList()); @@ -179,6 +185,30 @@ public List getDependentResources() { return specs; } + @SuppressWarnings("unchecked") + private void addConditions(DependentResourceSpec spec, Dependent dependent) { + if (dependent.deletePostcondition() != VoidCondition.class) { + spec.setDeletePostCondition(instantiateCondition(dependent.deletePostcondition())); + } + if (dependent.readyPostcondition() != VoidCondition.class) { + spec.setReadyPostcondition(instantiateCondition(dependent.readyPostcondition())); + } + if (dependent.reconcilePrecondition() != VoidCondition.class) { + spec.setReconcilePrecondition(instantiateCondition(dependent.reconcilePrecondition())); + } + } + + private Condition instantiateCondition(Class condition) { + try { + return condition.getDeclaredConstructor().newInstance(); + } catch (InstantiationException + | IllegalAccessException + | InvocationTargetException + | NoSuchMethodException e) { + throw new OperatorException(e); + } + } + private String getName(Dependent dependent, Class dependentType) { var name = dependent.name(); if (name.isBlank()) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java index 50419f6a5d..0a8503ae05 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProvider.java @@ -7,10 +7,8 @@ * to the ConfigurationService is via the reconciliation context. */ public class ConfigurationServiceProvider { - static final ConfigurationService DEFAULT = - new BaseConfigurationService(Utils.loadFromProperties()); private static ConfigurationService instance; - private static ConfigurationService defaultConfigurationService = DEFAULT; + private static ConfigurationService defaultConfigurationService = createDefault(); private static boolean alreadyConfigured = false; private ConfigurationServiceProvider() {} @@ -64,8 +62,12 @@ synchronized static ConfigurationService getDefault() { } public synchronized static void reset() { - defaultConfigurationService = DEFAULT; + defaultConfigurationService = createDefault(); instance = null; alreadyConfigured = false; } + + static ConfigurationService createDefault() { + return new BaseConfigurationService(Utils.loadFromProperties()); + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java index d81a7fcb03..88b511be31 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/config/ControllerConfigurationOverrider.java @@ -140,11 +140,10 @@ public ControllerConfiguration build() { // if the spec has a config and it's a KubernetesDependentResourceConfig, update the // namespaces if needed, otherwise, just return the existing spec final Optional maybeConfig = spec.getDependentResourceConfiguration(); - final Class drClass = drsEntry.getValue().getDependentResourceClass(); return maybeConfig.filter(KubernetesDependentResourceConfig.class::isInstance) .map(KubernetesDependentResourceConfig.class::cast) .filter(Predicate.not(KubernetesDependentResourceConfig::wereNamespacesConfigured)) - .map(c -> updateSpec(drsEntry.getKey(), drClass, c)) + .map(c -> updateSpec(drsEntry.getKey(), spec, c)) .orElse(drsEntry.getValue()); }).collect(Collectors.toUnmodifiableList()); @@ -164,9 +163,15 @@ public ControllerConfiguration build() { } @SuppressWarnings({"rawtypes", "unchecked"}) - private DependentResourceSpec updateSpec(String name, Class drClass, + private DependentResourceSpec updateSpec(String name, DependentResourceSpec spec, KubernetesDependentResourceConfig c) { - return new DependentResourceSpec(drClass, c.setNamespaces(namespaces), name); + var res = new DependentResourceSpec(spec.getDependentResourceClass(), + c.setNamespaces(namespaces), name); + res.setReadyPostcondition(spec.getReadyCondition()); + res.setReconcilePrecondition(spec.getReconcileCondition()); + res.setDeletePostCondition(spec.getDeletePostCondition()); + res.setDependsOn(spec.getDependsOn()); + return res; } public static ControllerConfigurationOverrider override( 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 40a7db53c9..2ef9e58de4 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 @@ -1,9 +1,12 @@ package io.javaoperatorsdk.operator.api.config.dependent; +import java.util.HashSet; import java.util.Objects; import java.util.Optional; +import java.util.Set; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; public class DependentResourceSpec, C> { @@ -13,6 +16,14 @@ public class DependentResourceSpec, C> { private final String name; + private Set dependsOn; + + private Condition readyCondition; + + private Condition reconcileCondition; + + private Condition deletePostCondition; + public DependentResourceSpec(Class dependentResourceClass, C dependentResourceConfig, String name) { this.dependentResourceClass = dependentResourceClass; @@ -55,4 +66,46 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(name); } + + public Set getDependsOn() { + if (dependsOn == null) { + dependsOn = new HashSet<>(0); + } + return dependsOn; + } + + public DependentResourceSpec setDependsOn(Set dependsOn) { + this.dependsOn = dependsOn; + return this; + } + + @SuppressWarnings("rawtypes") + public Condition getReadyCondition() { + return readyCondition; + } + + public DependentResourceSpec setReadyPostcondition(Condition readyCondition) { + this.readyCondition = readyCondition; + return this; + } + + @SuppressWarnings("rawtypes") + public Condition getReconcileCondition() { + return reconcileCondition; + } + + public DependentResourceSpec setReconcilePrecondition(Condition reconcileCondition) { + this.reconcileCondition = reconcileCondition; + return this; + } + + @SuppressWarnings("rawtypes") + public Condition getDeletePostCondition() { + return deletePostCondition; + } + + public DependentResourceSpec setDeletePostCondition(Condition deletePostCondition) { + this.deletePostCondition = deletePostCondition; + return this; + } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java index e2e70a246d..2a53d5730b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/Cleaner.java @@ -5,7 +5,7 @@ public interface Cleaner

{ /** - * Note that this method turns on automatic finalizer usage. + * This method turns on automatic finalizer usage. * * The implementation should delete the associated component(s). This method is called when an * object is marked for deletion. After it's executed the custom resource finalizer is diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java index 0b6d90065b..9891d358d4 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/DefaultContext.java @@ -6,6 +6,7 @@ import io.fabric8.kubernetes.api.model.HasMetadata; 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.processing.Controller; @@ -15,14 +16,14 @@ public class DefaultContext

implements Context

{ private final Controller

controller; private final P primaryResource; private final ControllerConfiguration

controllerConfiguration; - private final ManagedDependentResourceContext managedDependentResourceContext; + private final DefaultManagedDependentResourceContext defaultManagedDependentResourceContext; public DefaultContext(RetryInfo retryInfo, Controller

controller, P primaryResource) { this.retryInfo = retryInfo; this.controller = controller; this.primaryResource = primaryResource; this.controllerConfiguration = controller.getConfiguration(); - this.managedDependentResourceContext = new ManagedDependentResourceContext(); + this.defaultManagedDependentResourceContext = new DefaultManagedDependentResourceContext(); } @Override @@ -52,8 +53,9 @@ public ControllerConfiguration

getControllerConfiguration() { return controllerConfiguration; } + @Override public ManagedDependentResourceContext managedDependentResourceContext() { - return managedDependentResourceContext; + return defaultManagedDependentResourceContext; } public DefaultContext

setRetryInfo(RetryInfo retryInfo) { 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 25c7418445..0eba3fb598 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 @@ -1,5 +1,7 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; + import static io.javaoperatorsdk.operator.api.reconciler.Constants.NO_VALUE_SET; public @interface Dependent { @@ -8,4 +10,15 @@ Class type(); String name() default NO_VALUE_SET; + + @SuppressWarnings("rawtypes") + Class readyPostcondition() default VoidCondition.class; + + @SuppressWarnings("rawtypes") + Class reconcilePrecondition() default VoidCondition.class; + + @SuppressWarnings("rawtypes") + Class deletePostcondition() default VoidCondition.class; + + String[] dependsOn() default {}; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/VoidCondition.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/VoidCondition.java new file mode 100644 index 0000000000..7f4faf0ed1 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/VoidCondition.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; + +/** Used as default value for Condition in annotations */ +@SuppressWarnings("rawtypes") +public class VoidCondition implements Condition { + @Override + public boolean isMet(DependentResource dependentResource, HasMetadata primary, Context context) { + throw new IllegalStateException("This is a placeholder class, should not be called"); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DefaultManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DefaultManagedDependentResourceContext.java new file mode 100644 index 0000000000..5b1a21e5dd --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/DefaultManagedDependentResourceContext.java @@ -0,0 +1,61 @@ +package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; + +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult; +import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowReconcileResult; + +@SuppressWarnings("rawtypes") +public class DefaultManagedDependentResourceContext implements ManagedDependentResourceContext { + + private WorkflowReconcileResult workflowReconcileResult; + private WorkflowCleanupResult workflowCleanupResult; + private final ConcurrentHashMap attributes = new ConcurrentHashMap(); + + @Override + public Optional get(Object key, Class expectedType) { + return Optional.ofNullable(attributes.get(key)) + .filter(expectedType::isInstance) + .map(expectedType::cast); + } + + @Override + @SuppressWarnings("unchecked") + public T put(Object key, T value) { + if (value == null) { + return (T) Optional.ofNullable(attributes.remove(key)); + } + return (T) Optional.ofNullable(attributes.put(key, value)); + } + + @Override + @SuppressWarnings("unused") + public T getMandatory(Object key, Class expectedType) { + return get(key, expectedType).orElseThrow(() -> new IllegalStateException( + "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() + + ") is missing or not of the expected type")); + } + + public DefaultManagedDependentResourceContext setWorkflowExecutionResult( + WorkflowReconcileResult workflowReconcileResult) { + this.workflowReconcileResult = workflowReconcileResult; + return this; + } + + public DefaultManagedDependentResourceContext setWorkflowCleanupResult( + WorkflowCleanupResult workflowCleanupResult) { + this.workflowCleanupResult = workflowCleanupResult; + return this; + } + + @Override + public Optional getWorkflowReconcileResult() { + return Optional.ofNullable(workflowReconcileResult); + } + + @Override + public Optional getWorkflowCleanupResult() { + return Optional.ofNullable(workflowCleanupResult); + } +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java index 0d0b4c1412..42741c3a97 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/api/reconciler/dependent/managed/ManagedDependentResourceContext.java @@ -1,21 +1,16 @@ package io.javaoperatorsdk.operator.api.reconciler.dependent.managed; -import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; -import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; +import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult; +import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowReconcileResult; /** * 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 */ -@SuppressWarnings("rawtypes") -public class ManagedDependentResourceContext { - - private final Map reconcileResults = new ConcurrentHashMap<>(); - private final ConcurrentHashMap attributes = new ConcurrentHashMap(); +public interface ManagedDependentResourceContext { /** * Retrieve a contextual object, if it exists and is of the specified expected type, associated @@ -28,11 +23,7 @@ public class ManagedDependentResourceContext { * @return an Optional containing the contextual object or {@link Optional#empty()} if no such * object exists or doesn't match the expected type */ - public Optional get(Object key, Class expectedType) { - return Optional.ofNullable(attributes.get(key)) - .filter(expectedType::isInstance) - .map(expectedType::cast); - } + Optional get(Object key, Class expectedType); /** * Associates the specified contextual value to the specified key. If the value is {@code null}, @@ -46,12 +37,7 @@ public Optional get(Object key, Class expectedType) { * {@link Optional#empty()} if none existed */ @SuppressWarnings("unchecked") - public Optional put(Object key, Object value) { - if (value == null) { - return Optional.ofNullable(attributes.remove(key)); - } - return Optional.ofNullable(attributes.put(key, value)); - } + T put(Object key, T value); /** * Retrieves the value associated with the key or fail with an exception if none exists. @@ -63,35 +49,9 @@ public Optional put(Object key, Object value) { * @see #get(Object, Class) */ @SuppressWarnings("unused") - public T getMandatory(Object key, Class expectedType) { - return get(key, expectedType).orElseThrow(() -> new IllegalStateException( - "Mandatory attribute (key: " + key + ", type: " + expectedType.getName() - + ") is missing or not of the expected type")); - } + T getMandatory(Object key, Class expectedType); - /** - * Retrieve the {@link ReconcileResult}, if it exists, associated with the - * {@link DependentResource} associated with the specified name - * - * @param name the name of the {@link DependentResource} for which we want to retrieve a - * {@link ReconcileResult} - * @return an Optional containing the reconcile result or {@link Optional#empty()} if no such - * result is available - */ - @SuppressWarnings({"rawtypes", "unused"}) - public Optional getReconcileResult(String name) { - return Optional.ofNullable(reconcileResults.get(name)); - } + Optional getWorkflowReconcileResult(); - /** - * Set the {@link ReconcileResult} for the specified {@link DependentResource} implementation. - * - * @param name the name of the {@link DependentResource} for which we want to set the - * {@link ReconcileResult} - * @param reconcileResult the reconcile result associated with the specified - * {@link DependentResource} - */ - public void setReconcileResult(String name, ReconcileResult reconcileResult) { - reconcileResults.put(name, reconcileResult); - } + Optional getWorkflowCleanupResult(); } 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 f60dffcc33..5c25787f1f 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/Controller.java @@ -1,7 +1,7 @@ package io.javaoperatorsdk.operator.processing; -import java.util.*; -import java.util.stream.Collectors; +import java.util.Optional; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,20 +13,28 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.dsl.MixedOperation; import io.fabric8.kubernetes.client.dsl.Resource; -import io.javaoperatorsdk.operator.*; +import io.javaoperatorsdk.operator.CustomResourceUtils; +import io.javaoperatorsdk.operator.MissingCRDException; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.RegisteredController; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; -import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; 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.dependent.Deleter; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +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.EventSourceInitializer; +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.EventSourceProvider; -import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; -import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.ManagedDependentResourceException; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DefaultManagedDependentResourceContext; +import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow; +import io.javaoperatorsdk.operator.processing.dependent.workflow.WorkflowCleanupResult; import io.javaoperatorsdk.operator.processing.event.EventSourceManager; import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE; @@ -42,12 +50,10 @@ public class Controller

private final ControllerConfiguration

configuration; private final KubernetesClient kubernetesClient; private final EventSourceManager

eventSourceManager; - private final LinkedHashMap dependents; private final boolean contextInitializer; - private final boolean hasDeleterDependents; private final boolean isCleaner; private final Metrics metrics; - + private final ManagedWorkflow

managedWorkflow; public Controller(Reconciler

reconciler, ControllerConfiguration

configuration, @@ -58,95 +64,10 @@ public Controller(Reconciler

reconciler, this.metrics = Optional.ofNullable(ConfigurationServiceProvider.instance().getMetrics()) .orElse(Metrics.NOOP); contextInitializer = reconciler instanceof ContextInitializer; - eventSourceManager = new EventSourceManager<>(this); - - final var hasDeleterHolder = new boolean[] {false}; - final var specs = configuration.getDependentResources(); - final var specsSize = specs.size(); - if (specsSize == 0) { - dependents = new LinkedHashMap<>(); - } else { - final Map dependentsHolder = new LinkedHashMap<>(specsSize); - specs.forEach(drs -> { - final var dependent = createAndConfigureFrom(drs, kubernetesClient); - // check if dependent implements Deleter to record that fact - if (!hasDeleterHolder[0] && dependent instanceof Deleter - && !(dependent instanceof GarbageCollected)) { - hasDeleterHolder[0] = true; - } - dependentsHolder.put(drs.getName(), dependent); - }); - dependents = new LinkedHashMap<>(dependentsHolder); - } - - hasDeleterDependents = hasDeleterHolder[0]; isCleaner = reconciler instanceof Cleaner; - } - - @SuppressWarnings("rawtypes") - private DependentResource createAndConfigureFrom(DependentResourceSpec spec, - KubernetesClient client) { - final var dependentResource = - ConfigurationServiceProvider.instance().dependentResourceFactory().createFrom(spec); - - if (dependentResource instanceof KubernetesClientAware) { - ((KubernetesClientAware) dependentResource).setKubernetesClient(client); - } - - if (dependentResource instanceof DependentResourceConfigurator) { - final var configurator = (DependentResourceConfigurator) dependentResource; - spec.getDependentResourceConfiguration().ifPresent(configurator::configureWith); - } - return dependentResource; - } - - private void initContextIfNeeded(P resource, Context

context) { - if (contextInitializer) { - ((ContextInitializer

) reconciler).initContext(resource, context); - } - } - - @Override - public DeleteControl cleanup(P resource, Context

context) { - try { - return metrics - .timeControllerExecution( - new ControllerExecution<>() { - @Override - public String name() { - return "cleanup"; - } - - @Override - public String controllerName() { - return configuration.getName(); - } - - @Override - public String successTypeName(DeleteControl deleteControl) { - return deleteControl.isRemoveFinalizer() ? "delete" : "finalizerNotRemoved"; - } - - @Override - public DeleteControl execute() { - initContextIfNeeded(resource, context); - if (hasDeleterDependents) { - dependents.values().stream() - .filter(d -> d instanceof Deleter && !(d instanceof GarbageCollected)) - .map(Deleter.class::cast) - .forEach(deleter -> deleter.delete(resource, context)); - } - if (isCleaner) { - return ((Cleaner

) reconciler).cleanup(resource, context); - } else { - return DeleteControl.defaultDelete(); - } - } - }); - } catch (Exception e) { - throw new OperatorException(e); - } + managedWorkflow = + ManagedWorkflow.workflowFor(kubernetesClient, configuration.getDependentResources()); } @Override @@ -178,53 +99,84 @@ public String successTypeName(UpdateControl

result) { @Override public UpdateControl

execute() throws Exception { initContextIfNeeded(resource, context); - final var exceptions = new ArrayList(dependents.size()); - dependents.forEach((name, dependent) -> { - try { - final var reconcileResult = dependent.reconcile(resource, context); - context.managedDependentResourceContext().setReconcileResult(name, - reconcileResult); - log.info("Reconciled dependent '{}' -> {}", name, reconcileResult.getOperation()); - } catch (Exception e) { - final var message = e.getMessage(); - exceptions.add(new ManagedDependentResourceException( - name, "Error reconciling dependent '" + name + "': " + message, e)); - } - }); - - if (!exceptions.isEmpty()) { - throw new AggregatedOperatorException("One or more DependentResource(s) failed:\n" + - exceptions.stream() - .map(Controller.this::createExceptionInformation) - .collect(Collectors.joining("\n")), - exceptions); + if (!managedWorkflow.isEmptyWorkflow()) { + var res = managedWorkflow.reconcile(resource, context); + ((DefaultManagedDependentResourceContext) context.managedDependentResourceContext()) + .setWorkflowExecutionResult(res); + res.throwAggregateExceptionIfErrorsPresent(); } - return reconciler.reconcile(resource, context); } }); } - private String createExceptionInformation(Exception e) { - final var exceptionLocation = Optional.ofNullable(e.getCause()) - .map(Throwable::getStackTrace) - .filter(stackTrace -> stackTrace.length > 0) - .map(stackTrace -> { - int i = 0; - while (i < stackTrace.length) { - final var moduleName = stackTrace[i].getModuleName(); - if (!"java.base".equals(moduleName)) { - return " at: " + stackTrace[i].toString(); + @Override + public DeleteControl cleanup(P resource, Context

context) { + try { + return metrics.timeControllerExecution( + new ControllerExecution<>() { + @Override + public String name() { + return "cleanup"; } - i++; - } - return ""; - }); - return "\t\t- " + e.getMessage() + exceptionLocation.orElse(""); + + @Override + public String controllerName() { + return configuration.getName(); + } + + @Override + public String successTypeName(DeleteControl deleteControl) { + return deleteControl.isRemoveFinalizer() ? "delete" : "finalizerNotRemoved"; + } + + @Override + public DeleteControl execute() { + initContextIfNeeded(resource, context); + WorkflowCleanupResult workflowCleanupResult = null; + if (managedWorkflow.isCleaner()) { + workflowCleanupResult = managedWorkflow.cleanup(resource, context); + ((DefaultManagedDependentResourceContext) context.managedDependentResourceContext()) + .setWorkflowCleanupResult(workflowCleanupResult); + workflowCleanupResult.throwAggregateExceptionIfErrorsPresent(); + } + if (isCleaner) { + var cleanupResult = ((Cleaner

) reconciler).cleanup(resource, context); + if (!cleanupResult.isRemoveFinalizer()) { + return cleanupResult; + } else { + // this means there is no reschedule + return workflowCleanupResultToDefaultDelete(workflowCleanupResult); + } + } else { + return workflowCleanupResultToDefaultDelete(workflowCleanupResult); + } + } + }); + } catch (Exception e) { + throw new OperatorException(e); + } + } + + private DeleteControl workflowCleanupResultToDefaultDelete( + WorkflowCleanupResult workflowCleanupResult) { + if (workflowCleanupResult == null) { + return DeleteControl.defaultDelete(); + } else { + return workflowCleanupResult.allPostConditionsMet() ? DeleteControl.defaultDelete() + : DeleteControl.noFinalizerRemoval(); + } + } + + private void initContextIfNeeded(P resource, Context

context) { + if (contextInitializer) { + ((ContextInitializer

) reconciler).initContext(resource, context); + } } public void initAndRegisterEventSources(EventSourceContext

context) { - dependents.entrySet().stream() + managedWorkflow + .getDependentResourcesByName().entrySet().stream() .filter(drEntry -> drEntry.getValue() instanceof EventSourceProvider) .forEach(drEntry -> { final var provider = (EventSourceProvider) drEntry.getValue(); @@ -374,6 +326,6 @@ public void stop() { } public boolean useFinalizer() { - return isCleaner || hasDeleterDependents; + return isCleaner || managedWorkflow.isCleaner(); } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDNoGCKubernetesDependentResource.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDNoGCKubernetesDependentResource.java new file mode 100644 index 0000000000..e9b18c4734 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/kubernetes/CRUDNoGCKubernetesDependentResource.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.processing.dependent.kubernetes; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; + +public class CRUDNoGCKubernetesDependentResource + extends KubernetesDependentResource + implements Creator, Updater, Deleter

{ + + public CRUDNoGCKubernetesDependentResource(Class resourceType) { + super(resourceType); + } +} 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 new file mode 100644 index 0000000000..e1162ebcca --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/DefaultManagedWorkflow.java @@ -0,0 +1,65 @@ +package io.javaoperatorsdk.operator.processing.dependent.workflow; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; + +@SuppressWarnings("rawtypes") +public class DefaultManagedWorkflow

implements ManagedWorkflow

{ + + private final Workflow

workflow; + private final boolean isCleaner; + private final boolean isEmptyWorkflow; + private final Map dependentResourcesByName; + + DefaultManagedWorkflow(KubernetesClient client, + List dependentResourceSpecs, + ManagedWorkflowSupport managedWorkflowSupport) { + managedWorkflowSupport.checkForNameDuplication(dependentResourceSpecs); + dependentResourcesByName = dependentResourceSpecs + .stream().collect(Collectors.toMap(DependentResourceSpec::getName, + spec -> managedWorkflowSupport.createAndConfigureFrom(spec, client))); + + isEmptyWorkflow = dependentResourceSpecs.isEmpty(); + workflow = + managedWorkflowSupport.createWorkflow(dependentResourceSpecs, dependentResourcesByName); + isCleaner = checkIfCleaner(); + } + + public WorkflowReconcileResult reconcile(P primary, Context

context) { + return workflow.reconcile(primary, context); + } + + public WorkflowCleanupResult cleanup(P primary, Context

context) { + return workflow.cleanup(primary, context); + } + + private boolean checkIfCleaner() { + for (var dr : workflow.getDependentResources()) { + if (dr instanceof Deleter && !(dr instanceof GarbageCollected)) { + return true; + } + } + return false; + } + + public boolean isCleaner() { + return isCleaner; + } + + public boolean isEmptyWorkflow() { + return isEmptyWorkflow; + } + + public Map getDependentResourcesByName() { + return dependentResourcesByName; + } +} 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 973afd4ff6..d7af13bc43 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 @@ -3,7 +3,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Optional; -import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; @@ -12,9 +11,9 @@ public class DependentResourceNode { private final DependentResource dependentResource; - private Condition reconcileCondition; - private Condition deletePostCondition; - private Condition readyCondition; + private Condition reconcilePrecondition; + private Condition deletePostcondition; + private Condition readyPostcondition; private final List dependsOn = new LinkedList<>(); private final List parents = new LinkedList<>(); @@ -23,27 +22,27 @@ public DependentResourceNode(DependentResource dependentResource) { } public DependentResourceNode(DependentResource dependentResource, - Condition reconcileCondition) { - this(dependentResource, reconcileCondition, null); + Condition reconcilePrecondition) { + this(dependentResource, reconcilePrecondition, null); } public DependentResourceNode(DependentResource dependentResource, - Condition reconcileCondition, Condition deletePostCondition) { + Condition reconcilePrecondition, Condition deletePostcondition) { this.dependentResource = dependentResource; - this.reconcileCondition = reconcileCondition; - this.deletePostCondition = deletePostCondition; + this.reconcilePrecondition = reconcilePrecondition; + this.deletePostcondition = deletePostcondition; } public DependentResource getDependentResource() { return dependentResource; } - public Optional getReconcileCondition() { - return Optional.ofNullable(reconcileCondition); + public Optional getReconcilePrecondition() { + return Optional.ofNullable(reconcilePrecondition); } - public Optional getDeletePostCondition() { - return Optional.ofNullable(deletePostCondition); + public Optional getDeletePostcondition() { + return Optional.ofNullable(deletePostcondition); } public List getDependsOn() { @@ -58,32 +57,28 @@ public void addDependsOnRelation(DependentResourceNode node) { @Override public String toString() { - return "{" - + parents.stream().map(p -> p.dependentResource.toString()) - .collect(Collectors.joining(", ", "[", "]->")) - + "(" + dependentResource + ")" - + dependsOn.stream().map(d -> d.dependentResource.toString()) - .collect(Collectors.joining(", ", "->[", "]")) - + '}'; + return "DependentResourceNode{" + + "dependentResource=" + dependentResource + + '}'; } - public DependentResourceNode setReconcileCondition( - Condition reconcileCondition) { - this.reconcileCondition = reconcileCondition; + public DependentResourceNode setReconcilePrecondition( + Condition reconcilePrecondition) { + this.reconcilePrecondition = reconcilePrecondition; return this; } - public DependentResourceNode setDeletePostCondition(Condition cleanupCondition) { - this.deletePostCondition = cleanupCondition; + public DependentResourceNode setDeletePostcondition(Condition cleanupCondition) { + this.deletePostcondition = cleanupCondition; return this; } - public Optional> getReadyCondition() { - return Optional.ofNullable(readyCondition); + public Optional> getReadyPostcondition() { + return Optional.ofNullable(readyPostcondition); } - public DependentResourceNode setReadyCondition(Condition readyCondition) { - this.readyCondition = readyCondition; + public DependentResourceNode setReadyPostcondition(Condition readyPostcondition) { + this.readyPostcondition = readyPostcondition; return this; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java new file mode 100644 index 0000000000..08e2c497b0 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflow.java @@ -0,0 +1,62 @@ +package io.javaoperatorsdk.operator.processing.dependent.workflow; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; + +@SuppressWarnings("rawtypes") +public interface ManagedWorkflow

{ + + ManagedWorkflow noOpWorkflow = new ManagedWorkflow() { + @Override + public WorkflowReconcileResult reconcile(HasMetadata primary, Context context) { + throw new IllegalStateException("Shouldn't be called"); + } + + @Override + public WorkflowCleanupResult cleanup(HasMetadata primary, Context context) { + throw new IllegalStateException("Shouldn't be called"); + } + + @Override + public boolean isCleaner() { + return false; + } + + @Override + public boolean isEmptyWorkflow() { + return true; + } + + @Override + public Map getDependentResourcesByName() { + return Collections.emptyMap(); + } + }; + + @SuppressWarnings("unchecked") + static ManagedWorkflow workflowFor(KubernetesClient client, + List dependentResourceSpecs) { + if (dependentResourceSpecs == null || dependentResourceSpecs.isEmpty()) { + return noOpWorkflow; + } + return new DefaultManagedWorkflow(client, dependentResourceSpecs, + ManagedWorkflowSupport.instance()); + } + + WorkflowReconcileResult reconcile(P primary, Context

context); + + WorkflowCleanupResult cleanup(P primary, Context

context); + + boolean isCleaner(); + + boolean isEmptyWorkflow(); + + Map getDependentResourcesByName(); +} diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java new file mode 100644 index 0000000000..aad9475518 --- /dev/null +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupport.java @@ -0,0 +1,184 @@ +package io.javaoperatorsdk.operator.processing.dependent.workflow; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +import io.fabric8.kubernetes.api.model.HasMetadata; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.DependentResourceConfigurator; +import io.javaoperatorsdk.operator.api.reconciler.dependent.managed.KubernetesClientAware; +import io.javaoperatorsdk.operator.processing.dependent.workflow.builder.WorkflowBuilder; + +@SuppressWarnings({"rawtypes", "unchecked"}) +class ManagedWorkflowSupport { + + private final static ManagedWorkflowSupport instance = new ManagedWorkflowSupport(); + + static ManagedWorkflowSupport instance() { + return instance; + } + + private ManagedWorkflowSupport() {} + + public void checkForNameDuplication(List dependentResourceSpecs) { + if (dependentResourceSpecs == null) { + return; + } + final var size = dependentResourceSpecs.size(); + if (size == 0) { + return; + } + + final var uniqueNames = new HashSet<>(size); + final var duplicatedNames = new HashSet<>(size); + dependentResourceSpecs.forEach(spec -> { + final var name = spec.getName(); + if (!uniqueNames.add(name)) { + duplicatedNames.add(name); + } + }); + if (!duplicatedNames.isEmpty()) { + throw new OperatorException("Duplicated dependent resource name(s): " + duplicatedNames); + } + } + + @SuppressWarnings("unchecked") + public

Workflow

createWorkflow( + List dependentResourceSpecs, + Map dependentResourceByName) { + var orderedResourceSpecs = orderAndDetectCycles(dependentResourceSpecs); + var workflowBuilder = new WorkflowBuilder

().withThrowExceptionFurther(false); + orderedResourceSpecs.forEach(spec -> { + final var dependentResource = dependentResourceByName.get(spec.getName()); + final var dependsOn = (Set) spec.getDependsOn() + .stream().map(dependentResourceByName::get).collect(Collectors.toSet()); + workflowBuilder + .addDependentResource(dependentResource) + .dependsOn(dependsOn) + .withDeletePostcondition(spec.getDeletePostCondition()) + .withReconcilePrecondition(spec.getReconcileCondition()) + .withReadyPostcondition(spec.getReadyCondition()); + }); + return workflowBuilder.build(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public DependentResource createAndConfigureFrom(DependentResourceSpec spec, + KubernetesClient client) { + final var dependentResource = + ConfigurationServiceProvider.instance().dependentResourceFactory().createFrom(spec); + + if (dependentResource instanceof KubernetesClientAware) { + ((KubernetesClientAware) dependentResource).setKubernetesClient(client); + } + + if (dependentResource instanceof DependentResourceConfigurator) { + final var configurator = (DependentResourceConfigurator) dependentResource; + spec.getDependentResourceConfiguration().ifPresent(configurator::configureWith); + } + return dependentResource; + } + + /** + * + * @param dependentResourceSpecs list of specs + * @return top-bottom ordered resources that can be added safely to workflow + * @throws OperatorException if there is a cycle in the dependencies + * + */ + public List orderAndDetectCycles( + List dependentResourceSpecs) { + + final var drInfosByName = createDRInfos(dependentResourceSpecs); + final var orderedSpecs = new ArrayList(dependentResourceSpecs.size()); + final var alreadyVisited = new HashSet(); + var toVisit = getTopDependentResources(dependentResourceSpecs); + + while (!toVisit.isEmpty()) { + final var toVisitNext = new HashSet(); + toVisit.forEach(dr -> { + final var name = dr.getName(); + var drInfo = drInfosByName.get(name); + if (drInfo != null) { + drInfo.waitingForCompletion.forEach(spec -> { + if (isReadyForVisit(spec, alreadyVisited, name)) { + toVisitNext.add(spec); + } + }); + orderedSpecs.add(dr); + } + alreadyVisited.add(name); + }); + + toVisit = toVisitNext; + } + + if (orderedSpecs.size() != dependentResourceSpecs.size()) { + // could provide improved message where the exact cycles are made visible + throw new OperatorException("Cycle(s) between dependent resources."); + } + return orderedSpecs; + } + + private static class DRInfo { + private final DependentResourceSpec spec; + private final List waitingForCompletion; + + private DRInfo(DependentResourceSpec spec) { + this.spec = spec; + this.waitingForCompletion = new LinkedList<>(); + } + + void add(DependentResourceSpec spec) { + waitingForCompletion.add(spec); + } + + String name() { + return spec.getName(); + } + } + + private boolean isReadyForVisit(DependentResourceSpec dr, Set alreadyVisited, + String alreadyPresentName) { + for (var name : dr.getDependsOn()) { + if (name.equals(alreadyPresentName)) + continue; + if (!alreadyVisited.contains(name)) { + return false; + } + } + return true; + } + + private Set getTopDependentResources( + List dependentResourceSpecs) { + return dependentResourceSpecs.stream().filter(r -> r.getDependsOn().isEmpty()) + .collect(Collectors.toSet()); + } + + private Map createDRInfos(List dependentResourceSpecs) { + // first create mappings + final var infos = dependentResourceSpecs.stream() + .map(DRInfo::new) + .collect(Collectors.toMap(DRInfo::name, Function.identity())); + + // then populate the reverse depends on information + dependentResourceSpecs.forEach(spec -> spec.getDependsOn().forEach(name -> { + final var drInfo = infos.get(name); + drInfo.add(spec); + })); + + return infos; + } + +} 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 df7ca1bd31..bdd4e3cd7c 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 @@ -4,10 +4,12 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.config.ConfigurationServiceProvider; import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; /** * Dependents definition: so if B depends on A, the B is dependent of A. @@ -46,7 +48,7 @@ public Workflow(Set dependentResourceNodes, int globalPar this(dependentResourceNodes, Executors.newFixedThreadPool(globalParallelism), true); } - public WorkflowExecutionResult reconcile(P primary, Context

context) { + public WorkflowReconcileResult reconcile(P primary, Context

context) { WorkflowReconcileExecutor

workflowReconcileExecutor = new WorkflowReconcileExecutor<>(this, primary, context); var result = workflowReconcileExecutor.reconcile(); @@ -99,4 +101,9 @@ Set getBottomLevelResource() { ExecutorService getExecutorService() { return executorService; } + + public Set getDependentResources() { + return dependentResourceNodes.stream().map(DependentResourceNode::getDependentResource) + .collect(Collectors.toSet()); + } } 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 e56a496cda..27ff266ad6 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 @@ -78,7 +78,7 @@ private synchronized void handleCleanup(DependentResourceNode dependentResourceN workflow.getExecutorService().submit( new NodeExecutor(dependentResourceNode)); actualExecutions.put(dependentResourceNode, nodeFuture); - log.debug("Submitted to reconcile: {}", dependentResourceNode); + log.debug("Submitted for cleanup: {}", dependentResourceNode); } private class NodeExecutor implements Runnable { @@ -94,7 +94,7 @@ private NodeExecutor(DependentResourceNode dependentResourceNode) { public void run() { try { var dependentResource = dependentResourceNode.getDependentResource(); - var deletePostCondition = dependentResourceNode.getDeletePostCondition(); + var deletePostCondition = dependentResourceNode.getDeletePostcondition(); if (dependentResource instanceof Deleter && !(dependentResource instanceof GarbageCollected)) { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupResult.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupResult.java index c724376d78..f52774b9ac 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupResult.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowCleanupResult.java @@ -45,8 +45,8 @@ public WorkflowCleanupResult setErroredDependents( return this; } - public boolean postConditionsNotMet() { - return !postConditionNotMetDependents.isEmpty(); + public boolean allPostConditionsMet() { + return postConditionNotMetDependents.isEmpty(); } public boolean erroredDependentsExists() { 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 f32e3b83b8..0412e1c912 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 @@ -17,6 +17,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; +import io.javaoperatorsdk.operator.processing.event.ResourceID; @SuppressWarnings({"rawtypes", "unchecked"}) public class WorkflowReconcileExecutor

{ @@ -49,7 +50,7 @@ public WorkflowReconcileExecutor(Workflow

workflow, P primary, Context

con this.workflow = workflow; } - public synchronized WorkflowExecutionResult reconcile() { + public synchronized WorkflowReconcileResult reconcile() { for (DependentResourceNode dependentResourceNode : workflow .getTopLevelDependentResources()) { handleReconcile(dependentResourceNode); @@ -83,19 +84,15 @@ private synchronized void handleReconcile( return; } - boolean reconcileConditionMet = dependentResourceNode.getReconcileCondition().map( + boolean reconcileConditionMet = dependentResourceNode.getReconcilePrecondition().map( rc -> rc.isMet(dependentResourceNode.getDependentResource(), primary, context)) .orElse(true); if (!reconcileConditionMet) { handleReconcileConditionNotMet(dependentResourceNode); } else { - Future nodeFuture = - workflow - .getExecutorService() - .submit( - new NodeReconcileExecutor( - dependentResourceNode)); + Future nodeFuture = workflow.getExecutorService() + .submit(new NodeReconcileExecutor(dependentResourceNode)); actualExecutions.put(dependentResourceNode, nodeFuture); log.debug("Submitted to reconcile: {}", dependentResourceNode); } @@ -112,9 +109,8 @@ private void handleDelete(DependentResourceNode dependentResourceNode) { return; } - Future nodeFuture = - workflow.getExecutorService() - .submit(new NodeDeleteExecutor(dependentResourceNode)); + Future nodeFuture = workflow.getExecutorService() + .submit(new NodeDeleteExecutor(dependentResourceNode)); actualExecutions.put(dependentResourceNode, nodeFuture); log.debug("Submitted to delete: {}", dependentResourceNode); } @@ -160,11 +156,16 @@ private NodeReconcileExecutor(DependentResourceNode dependentResourceNode) public void run() { try { DependentResource dependentResource = dependentResourceNode.getDependentResource(); - + if (log.isDebugEnabled()) { + log.debug( + "Reconciling {} for primary: {}", + dependentResourceNode, + ResourceID.fromResource(primary)); + } ReconcileResult reconcileResult = dependentResource.reconcile(primary, context); reconcileResults.put(dependentResource, reconcileResult); reconciled.add(dependentResourceNode); - boolean ready = dependentResourceNode.getReadyCondition() + boolean ready = dependentResourceNode.getReadyPostcondition() .map(rc -> rc.isMet(dependentResource, primary, context)) .orElse(true); @@ -196,7 +197,7 @@ private NodeDeleteExecutor(DependentResourceNode dependentResourceNode) { public void run() { try { DependentResource dependentResource = dependentResourceNode.getDependentResource(); - var deletePostCondition = dependentResourceNode.getDeletePostCondition(); + var deletePostCondition = dependentResourceNode.getDeletePostcondition(); if (dependentResource instanceof Deleter && !(dependentResource instanceof GarbageCollected)) { @@ -280,18 +281,18 @@ private boolean hasErroredParent( .anyMatch(exceptionsDuringExecution::containsKey); } - private WorkflowExecutionResult createReconcileResult() { - WorkflowExecutionResult workflowExecutionResult = new WorkflowExecutionResult(); - workflowExecutionResult.setErroredDependents(exceptionsDuringExecution + private WorkflowReconcileResult createReconcileResult() { + WorkflowReconcileResult workflowReconcileResult = new WorkflowReconcileResult(); + workflowReconcileResult.setErroredDependents(exceptionsDuringExecution .entrySet().stream() .collect(Collectors.toMap(e -> e.getKey().getDependentResource(), Map.Entry::getValue))); - workflowExecutionResult.setNotReadyDependents(notReady.stream() + workflowReconcileResult.setNotReadyDependents(notReady.stream() .map(DependentResourceNode::getDependentResource) .collect(Collectors.toList())); - workflowExecutionResult.setReconciledDependents(reconciled.stream() + workflowReconcileResult.setReconciledDependents(reconciled.stream() .map(DependentResourceNode::getDependentResource).collect(Collectors.toList())); - workflowExecutionResult.setReconcileResults(reconcileResults); - return workflowExecutionResult; + workflowReconcileResult.setReconcileResults(reconcileResults); + return workflowReconcileResult; } } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowExecutionResult.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileResult.java similarity index 85% rename from operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowExecutionResult.java rename to operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileResult.java index 87c23e422c..3c75873920 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowExecutionResult.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowReconcileResult.java @@ -9,7 +9,7 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; @SuppressWarnings("rawtypes") -public class WorkflowExecutionResult { +public class WorkflowReconcileResult { private List reconciledDependents; private List notReadyDependents; @@ -20,7 +20,7 @@ public Map getErroredDependents() { return erroredDependents; } - public WorkflowExecutionResult setErroredDependents( + public WorkflowReconcileResult setErroredDependents( Map erroredDependents) { this.erroredDependents = erroredDependents; return this; @@ -30,7 +30,7 @@ public List getReconciledDependents() { return reconciledDependents; } - public WorkflowExecutionResult setReconciledDependents( + public WorkflowReconcileResult setReconciledDependents( List reconciledDependents) { this.reconciledDependents = reconciledDependents; return this; @@ -40,7 +40,7 @@ public List getNotReadyDependents() { return notReadyDependents; } - public WorkflowExecutionResult setNotReadyDependents( + public WorkflowReconcileResult setNotReadyDependents( List notReadyDependents) { this.notReadyDependents = notReadyDependents; return this; @@ -50,7 +50,7 @@ public Map getReconcileResults() { return reconcileResults; } - public WorkflowExecutionResult setReconcileResults( + public WorkflowReconcileResult setReconcileResults( Map reconcileResults) { this.reconcileResults = reconcileResults; return this; @@ -67,8 +67,8 @@ private AggregatedOperatorException createFinalException() { new ArrayList<>(erroredDependents.values())); } - public boolean notReadyDependentsExists() { - return !notReadyDependents.isEmpty(); + public boolean allDependentResourcesReady() { + return notReadyDependents.isEmpty(); } public boolean erroredDependentsExists() { diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/builder/DependentBuilder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/builder/DependentBuilder.java index 56feec0ae1..70991dc91d 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/builder/DependentBuilder.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/builder/DependentBuilder.java @@ -1,5 +1,9 @@ package io.javaoperatorsdk.operator.processing.dependent.workflow.builder; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + import io.fabric8.kubernetes.api.model.HasMetadata; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; @@ -16,7 +20,7 @@ public DependentBuilder(WorkflowBuilder

workflowBuilder, DependentResourceNod this.node = node; } - public DependentBuilder

dependsOn(DependentResource... dependentResources) { + public DependentBuilder

dependsOn(Set dependentResources) { for (var dependentResource : dependentResources) { var dependsOn = workflowBuilder.getNodeByDependentResource(dependentResource); node.addDependsOnRelation(dependsOn); @@ -24,18 +28,25 @@ public DependentBuilder

dependsOn(DependentResource... dependentResources) { return this; } - public DependentBuilder

withReconcileCondition(Condition reconcileCondition) { - node.setReconcileCondition(reconcileCondition); + public DependentBuilder

dependsOn(DependentResource... dependentResources) { + if (dependentResources != null) { + return dependsOn(new HashSet<>(Arrays.asList(dependentResources))); + } + return this; + } + + public DependentBuilder

withReconcilePrecondition(Condition reconcilePrecondition) { + node.setReconcilePrecondition(reconcilePrecondition); return this; } - public DependentBuilder

withReadyCondition(Condition readyCondition) { - node.setReadyCondition(readyCondition); + public DependentBuilder

withReadyPostcondition(Condition readyPostcondition) { + node.setReadyPostcondition(readyPostcondition); return this; } - public DependentBuilder

withDeletePostCondition(Condition readyCondition) { - node.setDeletePostCondition(readyCondition); + public DependentBuilder

withDeletePostcondition(Condition deletePostcondition) { + node.setDeletePostcondition(deletePostcondition); return this; } diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/builder/WorkflowBuilder.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/builder/WorkflowBuilder.java index 270dbd0261..14d1b96f76 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/builder/WorkflowBuilder.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/dependent/workflow/builder/WorkflowBuilder.java @@ -18,7 +18,7 @@ public class WorkflowBuilder

{ private final Set> dependentResourceNodes = new HashSet<>(); private boolean throwExceptionAutomatically = THROW_EXCEPTION_AUTOMATICALLY_DEFAULT; - public DependentBuilder

addDependent(DependentResource dependentResource) { + public DependentBuilder

addDependentResource(DependentResource dependentResource) { DependentResourceNode node = new DependentResourceNode<>(dependentResource); dependentResourceNodes.add(node); return new DependentBuilder<>(this, node); diff --git a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java index 612cac8e41..7baeab6a4b 100644 --- a/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java +++ b/operator-framework-core/src/main/java/io/javaoperatorsdk/operator/processing/event/ResourceID.java @@ -61,9 +61,18 @@ public int hashCode() { @Override public String toString() { + return toString(name, namespace); + } + + public static String toString(HasMetadata resource) { + return toString(resource.getMetadata().getName(), resource.getMetadata().getNamespace()); + } + + private static String toString(String name, String namespace) { return "ResourceID{" + "name='" + name + '\'' + ", namespace='" + namespace + '\'' + '}'; } + } 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 47fe4abcf3..bdf2096465 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 @@ -93,7 +93,9 @@ public InformerEventSource(InformerConfiguration configuration, KubernetesCli @Override public void onAdd(R resource) { if (log.isDebugEnabled()) { - log.debug("On add event received for resource id: {}", ResourceID.fromResource(resource)); + log.debug("On add event received for resource id: {} type: {}", + ResourceID.fromResource(resource), + resourceType().getSimpleName()); } primaryToSecondaryIndex.onAddOrUpdate(resource); onAddOrUpdate("add", resource, () -> InformerEventSource.super.onAdd(resource)); @@ -102,7 +104,9 @@ public void onAdd(R resource) { @Override public void onUpdate(R oldObject, R newObject) { if (log.isDebugEnabled()) { - log.debug("On update event received for resource id: {}", ResourceID.fromResource(newObject)); + log.debug("On update event received for resource id: {} type: {}", + ResourceID.fromResource(newObject), + resourceType().getSimpleName()); } primaryToSecondaryIndex.onAddOrUpdate(newObject); onAddOrUpdate("update", newObject, @@ -112,7 +116,9 @@ public void onUpdate(R oldObject, R newObject) { @Override public void onDelete(R resource, boolean b) { if (log.isDebugEnabled()) { - log.debug("On delete event received for resource id: {}", ResourceID.fromResource(resource)); + log.debug("On delete event received for resource id: {} type: {}", + ResourceID.fromResource(resource), + resourceType().getSimpleName()); } primaryToSecondaryIndex.onDelete(resource); super.onDelete(resource, b); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java index f8c1a0f6aa..fbe5a3542e 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/ConfigurationServiceProviderTest.java @@ -55,7 +55,9 @@ void resetShouldResetAllState() { shouldBePossibleToOverrideConfigOnce(); ConfigurationServiceProvider.reset(); - assertEquals(ConfigurationServiceProvider.DEFAULT, ConfigurationServiceProvider.getDefault()); + // makes sure createDefault creates a new instance + assertNotEquals(ConfigurationServiceProvider.getDefault(), + ConfigurationServiceProvider.createDefault()); shouldBePossibleToOverrideConfigOnce(); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java index 05ab4d1258..c9346134a3 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/api/config/UtilsTest.java @@ -8,6 +8,7 @@ import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; +import io.javaoperatorsdk.operator.processing.dependent.EmptyTestDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; @@ -84,7 +85,7 @@ void getsFirstTypeArgumentFromExtendedClass() { @Test void getsFirstTypeArgumentFromInterface() { - assertThat(Utils.getFirstTypeArgumentFromInterface(TestDependentResource.class)) + assertThat(Utils.getFirstTypeArgumentFromInterface(EmptyTestDependentResource.class)) .isEqualTo(Deployment.class); } diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/EmptyTestDependentResource.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/EmptyTestDependentResource.java new file mode 100644 index 0000000000..aa75849051 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/EmptyTestDependentResource.java @@ -0,0 +1,31 @@ +package io.javaoperatorsdk.operator.processing.dependent; + +import java.util.Optional; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.ReconcileResult; +import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; + +public class EmptyTestDependentResource + implements DependentResource { + + @Override + public ReconcileResult reconcile(TestCustomResource primary, + Context context) { + return null; + } + + @Override + public Optional getSecondaryResource(TestCustomResource primaryResource) { + return Optional.empty(); + } + + @Override + public Class resourceType() { + return Deployment.class; + } + +} + diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java new file mode 100644 index 0000000000..85f04f220f --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowSupportTest.java @@ -0,0 +1,159 @@ +package io.javaoperatorsdk.operator.processing.dependent.workflow; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.assertj.core.data.Index; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.OperatorException; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; + +import static io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowTestUtils.createDRS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +class ManagedWorkflowSupportTest { + + public static final String NAME_1 = "name1"; + public static final String NAME_2 = "name2"; + public static final String NAME_3 = "name3"; + public static final String NAME_4 = "name4"; + + ManagedWorkflowSupport managedWorkflowSupport = ManagedWorkflowSupport.instance(); + + @Test + void trivialCasesNameDuplicates() { + managedWorkflowSupport.checkForNameDuplication(null); + managedWorkflowSupport.checkForNameDuplication(Collections.emptyList()); + managedWorkflowSupport.checkForNameDuplication(List.of(createDRS(NAME_1))); + managedWorkflowSupport.checkForNameDuplication(List.of(createDRS(NAME_1), createDRS(NAME_2))); + } + + @Test + void checkFindsDuplicates() { + final var drs2 = createDRS(NAME_2); + final var drs1 = createDRS(NAME_1); + + Assertions.assertThrows(OperatorException.class, () -> managedWorkflowSupport + .checkForNameDuplication(List.of(drs2, drs2))); + + Assertions.assertThrows(OperatorException.class, + () -> managedWorkflowSupport.checkForNameDuplication( + List.of(drs1, drs2, drs2))); + + final var exception = Assertions.assertThrows(OperatorException.class, + () -> managedWorkflowSupport.checkForNameDuplication( + List.of(drs1, drs2, drs2, drs1))); + assertThat(exception.getMessage()).contains(NAME_1, NAME_2); + } + + @Test + void orderingTrivialCases() { + assertThat(managedWorkflowSupport.orderAndDetectCycles(List.of(createDRS(NAME_1)))) + .map(DependentResourceSpec::getName).containsExactly(NAME_1); + + assertThat(managedWorkflowSupport + .orderAndDetectCycles(List.of(createDRS(NAME_2, NAME_1), createDRS(NAME_1)))) + .map(DependentResourceSpec::getName).containsExactly(NAME_1, NAME_2); + } + + @Test + void orderingDiamondShape() { + String NAME_3 = "name3"; + String NAME_4 = "name4"; + + var res = managedWorkflowSupport + .orderAndDetectCycles(List.of(createDRS(NAME_2, NAME_1), createDRS(NAME_1), + createDRS(NAME_3, NAME_1), createDRS(NAME_4, NAME_2, NAME_3))) + .stream().map(DependentResourceSpec::getName).collect(Collectors.toList()); + + assertThat(res) + .containsExactlyInAnyOrder(NAME_1, NAME_2, NAME_3, NAME_4) + .contains(NAME_1, Index.atIndex(0)) + .contains(NAME_4, Index.atIndex(3)); + } + + + @Test + void orderingMultipleRoots() { + final var NAME_3 = "name3"; + final var NAME_4 = "name4"; + final var NAME_5 = "name5"; + final var NAME_6 = "name6"; + + var res = managedWorkflowSupport + .orderAndDetectCycles(List.of( + createDRS(NAME_2, NAME_1, NAME_5), + createDRS(NAME_1), + createDRS(NAME_3, NAME_1), + createDRS(NAME_4, NAME_2, NAME_3), + createDRS(NAME_5, NAME_1, NAME_6), + createDRS(NAME_6))) + .stream().map(DependentResourceSpec::getName).collect(Collectors.toList()); + + assertThat(res) + .containsExactlyInAnyOrder(NAME_1, NAME_5, NAME_6, NAME_2, NAME_3, NAME_4) + .contains(NAME_6, Index.atIndex(0)) + .contains(NAME_1, Index.atIndex(1)) + .contains(NAME_5, Index.atIndex(2)) + .contains(NAME_3, Index.atIndex(3)) + .contains(NAME_2, Index.atIndex(4)) + .contains(NAME_4, Index.atIndex(5)); + } + + @Test + void detectsCyclesTrivialCases() { + String NAME_3 = "name3"; + Assertions.assertThrows(OperatorException.class, () -> managedWorkflowSupport + .orderAndDetectCycles(List.of(createDRS(NAME_2, NAME_1), createDRS(NAME_1, NAME_2)))); + Assertions.assertThrows(OperatorException.class, + () -> managedWorkflowSupport + .orderAndDetectCycles(List.of(createDRS(NAME_2, NAME_1), createDRS(NAME_1, NAME_3), + createDRS(NAME_3, NAME_2)))); + } + + @Test + void detectsCycleOnSubTree() { + + Assertions.assertThrows(OperatorException.class, + () -> managedWorkflowSupport.orderAndDetectCycles(List.of(createDRS(NAME_1), + createDRS(NAME_2, NAME_1), + createDRS(NAME_3, NAME_1, NAME_4), + createDRS(NAME_4, NAME_3)))); + + Assertions.assertThrows(OperatorException.class, + () -> managedWorkflowSupport.orderAndDetectCycles(List.of( + createDRS(NAME_1), + createDRS(NAME_2, NAME_1, NAME_4), + createDRS(NAME_3, NAME_2), + createDRS(NAME_4, NAME_3)))); + } + + @Test + void createsWorkflow() { + var specs = List.of(createDRS(NAME_1), + createDRS(NAME_2, NAME_1), + createDRS(NAME_3, NAME_1), + createDRS(NAME_4, NAME_3, NAME_2)); + + var drByName = specs + .stream().collect(Collectors.toMap(DependentResourceSpec::getName, + spec -> managedWorkflowSupport.createAndConfigureFrom(spec, + mock(KubernetesClient.class)))); + + var workflow = managedWorkflowSupport.createWorkflow(specs, drByName); + + assertThat(workflow.getDependentResources()).containsExactlyInAnyOrder(drByName.values() + .toArray(new DependentResource[0])); + assertThat(workflow.getTopLevelDependentResources()) + .map(DependentResourceNode::getDependentResource).containsExactly(drByName.get(NAME_1)); + assertThat(workflow.getBottomLevelResource()).map(DependentResourceNode::getDependentResource) + .containsExactly(drByName.get(NAME_4)); + } + +} diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java new file mode 100644 index 0000000000..df556885b1 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTest.java @@ -0,0 +1,65 @@ +package io.javaoperatorsdk.operator.processing.dependent.workflow; + +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; +import io.javaoperatorsdk.operator.api.reconciler.dependent.GarbageCollected; + +import static io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowTestUtils.createDRS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +@SuppressWarnings({"rawtypes", "unchecked"}) +class ManagedWorkflowTest { + + public static final String NAME = "name"; + + ManagedWorkflowSupport managedWorkflowSupportMock = mock(ManagedWorkflowSupport.class); + KubernetesClient kubernetesClientMock = mock(KubernetesClient.class); + + @Test + void checksIfWorkflowEmpty() { + var mockWorkflow = mock(Workflow.class); + when(managedWorkflowSupportMock.createWorkflow(any(), any())).thenReturn(mockWorkflow); + when(managedWorkflowSupportMock.createAndConfigureFrom(any(), any())) + .thenReturn(mock(DependentResource.class)); + assertThat(managedWorkflow().isEmptyWorkflow()).isTrue(); + + when(mockWorkflow.getDependentResources()).thenReturn(Set.of(mock(DependentResource.class))); + assertThat(managedWorkflow(createDRS(NAME)).isEmptyWorkflow()).isFalse(); + } + + @Test + void isCleanerIfAtLeastOneDRIsDeleterAndNoGC() { + var mockWorkflow = mock(Workflow.class); + when(managedWorkflowSupportMock.createWorkflow(any(), any())).thenReturn(mockWorkflow); + when(managedWorkflowSupportMock.createAndConfigureFrom(any(), any())) + .thenReturn(mock(DependentResource.class)); + when(mockWorkflow.getDependentResources()).thenReturn(Set.of(mock(DependentResource.class))); + + assertThat(managedWorkflow(createDRS(NAME)).isCleaner()).isFalse(); + + when(mockWorkflow.getDependentResources()).thenReturn( + Set.of(mock(DependentResource.class, withSettings().extraInterfaces(Deleter.class)))); + assertThat(managedWorkflow(createDRS(NAME)).isCleaner()).isTrue(); + + when(mockWorkflow.getDependentResources()).thenReturn(Set.of(mock(DependentResource.class, + withSettings().extraInterfaces(Deleter.class, GarbageCollected.class)))); + assertThat(managedWorkflow(createDRS(NAME)).isCleaner()).isFalse(); + } + + ManagedWorkflow managedWorkflow(DependentResourceSpec... specs) { + return new DefaultManagedWorkflow(kubernetesClientMock, List.of(specs), + managedWorkflowSupportMock); + } + +} 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 new file mode 100644 index 0000000000..85d57db7a0 --- /dev/null +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/ManagedWorkflowTestUtils.java @@ -0,0 +1,19 @@ +package io.javaoperatorsdk.operator.processing.dependent.workflow; + +import java.util.Set; + +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.processing.dependent.EmptyTestDependentResource; + +@SuppressWarnings("rawtypes") +public class ManagedWorkflowTestUtils { + + @SuppressWarnings("unchecked") + public static DependentResourceSpec createDRS(String name, String... dependOns) { + final var spec = new DependentResourceSpec(EmptyTestDependentResource.class, + null, name); + spec.setDependsOn(Set.of(dependOns)); + return spec; + } + +} 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 2488059beb..6540f00157 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 @@ -19,10 +19,10 @@ class WorkflowCleanupExecutorTest extends AbstractWorkflowExecutorTest { @Test void cleanUpDiamondWorkflow() { var workflow = new WorkflowBuilder() - .addDependent(dd1).build() - .addDependent(dr1).dependsOn(dd1).build() - .addDependent(dd2).dependsOn(dd1).build() - .addDependent(dd3).dependsOn(dr1, dd2).build() + .addDependentResource(dd1).build() + .addDependentResource(dr1).dependsOn(dd1).build() + .addDependentResource(dd2).dependsOn(dd1).build() + .addDependentResource(dd3).dependsOn(dr1, dd2).build() .build(); var res = workflow.cleanup(new TestCustomResource(), null); @@ -38,10 +38,10 @@ void cleanUpDiamondWorkflow() { @Test void dontDeleteIfDependentErrored() { var workflow = new WorkflowBuilder() - .addDependent(dd1).build() - .addDependent(dd2).dependsOn(dd1).build() - .addDependent(dd3).dependsOn(dd2).build() - .addDependent(errorDD).dependsOn(dd2).build() + .addDependentResource(dd1).build() + .addDependentResource(dd2).dependsOn(dd1).build() + .addDependentResource(dd3).dependsOn(dd2).build() + .addDependentResource(errorDD).dependsOn(dd2).build() .withThrowExceptionFurther(false) .build(); @@ -60,8 +60,9 @@ void dontDeleteIfDependentErrored() { @Test void cleanupConditionTrivialCase() { var workflow = new WorkflowBuilder() - .addDependent(dd1).build() - .addDependent(dd2).dependsOn(dd1).withDeletePostCondition(noMetDeletePostCondition).build() + .addDependentResource(dd1).build() + .addDependentResource(dd2).dependsOn(dd1).withDeletePostcondition(noMetDeletePostCondition) + .build() .build(); var res = workflow.cleanup(new TestCustomResource(), null); @@ -75,8 +76,9 @@ void cleanupConditionTrivialCase() { @Test void cleanupConditionMet() { var workflow = new WorkflowBuilder() - .addDependent(dd1).build() - .addDependent(dd2).dependsOn(dd1).withDeletePostCondition(metDeletePostCondition).build() + .addDependentResource(dd1).build() + .addDependentResource(dd2).dependsOn(dd1).withDeletePostcondition(metDeletePostCondition) + .build() .build(); var res = workflow.cleanup(new TestCustomResource(), null); @@ -93,10 +95,11 @@ void cleanupConditionDiamondWorkflow() { TestDeleterDependent dd4 = new TestDeleterDependent("DR_DELETER_4"); var workflow = new WorkflowBuilder() - .addDependent(dd1).build() - .addDependent(dd2).dependsOn(dd1).build() - .addDependent(dd3).dependsOn(dd1).withDeletePostCondition(noMetDeletePostCondition).build() - .addDependent(dd4).dependsOn(dd2, dd3).build() + .addDependentResource(dd1).build() + .addDependentResource(dd2).dependsOn(dd1).build() + .addDependentResource(dd3).dependsOn(dd1).withDeletePostcondition(noMetDeletePostCondition) + .build() + .addDependentResource(dd4).dependsOn(dd2, dd3).build() .build(); var res = workflow.cleanup(new TestCustomResource(), null); @@ -116,7 +119,7 @@ void cleanupConditionDiamondWorkflow() { void dontDeleteIfGarbageCollected() { GarbageCollectedDeleter gcDel = new GarbageCollectedDeleter("GC_DELETER"); var workflow = new WorkflowBuilder() - .addDependent(gcDel).build() + .addDependentResource(gcDel).build() .build(); var res = workflow.cleanup(new TestCustomResource(), null); 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 b0d210b07e..2c81bcb50b 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 @@ -28,8 +28,8 @@ class WorkflowReconcileExecutorTest extends AbstractWorkflowExecutorTest { @Test void reconcileTopLevelResources() { var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(dr2).build() + .addDependentResource(dr1).build() + .addDependentResource(dr2).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -42,8 +42,8 @@ void reconcileTopLevelResources() { @Test void reconciliationWithSimpleDependsOn() { var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(dr2).dependsOn(dr1).build() + .addDependentResource(dr1).build() + .addDependentResource(dr2).dependsOn(dr1).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -60,9 +60,9 @@ void reconciliationWithTwoTheDependsOns() { TestDependent dr3 = new TestDependent("DR_3"); var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(dr2).dependsOn(dr1).build() - .addDependent(dr3).dependsOn(dr1).build() + .addDependentResource(dr1).build() + .addDependentResource(dr2).dependsOn(dr1).build() + .addDependentResource(dr3).dependsOn(dr1).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -81,10 +81,10 @@ void diamondShareWorkflowReconcile() { TestDependent dr4 = new TestDependent("DR_4"); var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(dr2).dependsOn(dr1).build() - .addDependent(dr3).dependsOn(dr1).build() - .addDependent(dr4).dependsOn(dr3).dependsOn(dr2).build() + .addDependentResource(dr1).build() + .addDependentResource(dr2).dependsOn(dr1).build() + .addDependentResource(dr3).dependsOn(dr1).build() + .addDependentResource(dr4).dependsOn(dr3).dependsOn(dr2).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -103,7 +103,7 @@ void diamondShareWorkflowReconcile() { @Test void exceptionHandlingSimpleCases() { var workflow = new WorkflowBuilder() - .addDependent(drError).build() + .addDependentResource(drError).build() .withThrowExceptionFurther(false) .build(); @@ -121,9 +121,9 @@ void exceptionHandlingSimpleCases() { @Test void dependentsOnErroredResourceNotReconciled() { var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(drError).dependsOn(dr1).build() - .addDependent(dr2).dependsOn(drError).build() + .addDependentResource(dr1).build() + .addDependentResource(drError).dependsOn(dr1).build() + .addDependentResource(dr2).dependsOn(drError).build() .withThrowExceptionFurther(false) .build(); @@ -142,10 +142,10 @@ void oneBranchErrorsOtherCompletes() { TestDependent dr3 = new TestDependent("DR_3"); var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(drError).dependsOn(dr1).build() - .addDependent(dr2).dependsOn(dr1).build() - .addDependent(dr3).dependsOn(dr2).build() + .addDependentResource(dr1).build() + .addDependentResource(drError).dependsOn(dr1).build() + .addDependentResource(dr2).dependsOn(dr1).build() + .addDependentResource(dr3).dependsOn(dr2).build() .withThrowExceptionFurther(false) .build(); @@ -162,9 +162,9 @@ void oneBranchErrorsOtherCompletes() { @Test void onlyOneDependsOnErroredResourceNotReconciled() { var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(drError).build() - .addDependent(dr2).dependsOn(drError, dr1).build() + .addDependentResource(dr1).build() + .addDependentResource(drError).build() + .addDependentResource(dr2).dependsOn(drError, dr1).build() .withThrowExceptionFurther(false) .build(); @@ -181,9 +181,10 @@ void onlyOneDependsOnErroredResourceNotReconciled() { @Test void simpleReconcileCondition() { var workflow = new WorkflowBuilder() - .addDependent(dr1).withReconcileCondition(not_met_reconcile_condition).build() - .addDependent(dr2).withReconcileCondition(met_reconcile_condition).build() - .addDependent(drDeleter).withReconcileCondition(not_met_reconcile_condition).build() + .addDependentResource(dr1).withReconcilePrecondition(not_met_reconcile_condition).build() + .addDependentResource(dr2).withReconcilePrecondition(met_reconcile_condition).build() + .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -198,9 +199,10 @@ void simpleReconcileCondition() { @Test void triangleOnceConditionNotMet() { var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(dr2).dependsOn(dr1).build() - .addDependent(drDeleter).withReconcileCondition(not_met_reconcile_condition).dependsOn(dr1) + .addDependentResource(dr1).build() + .addDependentResource(dr2).dependsOn(dr1).build() + .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .dependsOn(dr1) .build() .build(); @@ -217,13 +219,15 @@ void reconcileConditionTransitiveDelete() { TestDeleterDependent drDeleter2 = new TestDeleterDependent("DR_DELETER_2"); var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(dr2).dependsOn(dr1).withReconcileCondition(not_met_reconcile_condition) + .addDependentResource(dr1).build() + .addDependentResource(dr2).dependsOn(dr1) + .withReconcilePrecondition(not_met_reconcile_condition) .build() - .addDependent(drDeleter).dependsOn(dr2).withReconcileCondition(met_reconcile_condition) + .addDependentResource(drDeleter).dependsOn(dr2) + .withReconcilePrecondition(met_reconcile_condition) .build() - .addDependent(drDeleter2).dependsOn(drDeleter) - .withReconcileCondition(met_reconcile_condition).build() + .addDependentResource(drDeleter2).dependsOn(drDeleter) + .withReconcilePrecondition(met_reconcile_condition).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -243,10 +247,11 @@ void reconcileConditionAlsoErrorDependsOn() { TestDeleterDependent drDeleter2 = new TestDeleterDependent("DR_DELETER_2"); var workflow = new WorkflowBuilder() - .addDependent(drError).build() - .addDependent(drDeleter).withReconcileCondition(not_met_reconcile_condition).build() - .addDependent(drDeleter2).dependsOn(drError, drDeleter) - .withReconcileCondition(met_reconcile_condition) + .addDependentResource(drError).build() + .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .build() + .addDependentResource(drDeleter2).dependsOn(drError, drDeleter) + .withReconcilePrecondition(met_reconcile_condition) .build() .withThrowExceptionFurther(false) .build(); @@ -267,9 +272,9 @@ void reconcileConditionAlsoErrorDependsOn() { @Test void oneDependsOnConditionNotMet() { var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(dr2).withReconcileCondition(not_met_reconcile_condition).build() - .addDependent(drDeleter).dependsOn(dr1, dr2).build() + .addDependentResource(dr1).build() + .addDependentResource(dr2).withReconcilePrecondition(not_met_reconcile_condition).build() + .addDependentResource(drDeleter).dependsOn(dr1, dr2).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -286,10 +291,11 @@ void oneDependsOnConditionNotMet() { void deletedIfReconcileConditionNotMet() { TestDeleterDependent drDeleter2 = new TestDeleterDependent("DR_DELETER_2"); var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(drDeleter).dependsOn(dr1).withReconcileCondition(not_met_reconcile_condition) + .addDependentResource(dr1).build() + .addDependentResource(drDeleter).dependsOn(dr1) + .withReconcilePrecondition(not_met_reconcile_condition) .build() - .addDependent(drDeleter2).dependsOn(dr1, drDeleter).build() + .addDependentResource(drDeleter2).dependsOn(dr1, drDeleter).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -310,12 +316,13 @@ void deleteDoneInReverseOrder() { TestDeleterDependent drDeleter4 = new TestDeleterDependent("DR_DELETER_4"); var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(drDeleter).withReconcileCondition(not_met_reconcile_condition).dependsOn(dr1) + .addDependentResource(dr1).build() + .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .dependsOn(dr1) .build() - .addDependent(drDeleter2).dependsOn(drDeleter).build() - .addDependent(drDeleter3).dependsOn(drDeleter).build() - .addDependent(drDeleter4).dependsOn(drDeleter3).build() + .addDependentResource(drDeleter2).dependsOn(drDeleter).build() + .addDependentResource(drDeleter3).dependsOn(drDeleter).build() + .addDependentResource(drDeleter4).dependsOn(drDeleter3).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -337,11 +344,12 @@ void diamondDeleteWithPostConditionInMiddle() { TestDeleterDependent drDeleter4 = new TestDeleterDependent("DR_DELETER_4"); var workflow = new WorkflowBuilder() - .addDependent(drDeleter).withReconcileCondition(not_met_reconcile_condition).build() - .addDependent(drDeleter2).dependsOn(drDeleter).build() - .addDependent(drDeleter3).dependsOn(drDeleter) - .withDeletePostCondition(noMetDeletePostCondition).build() - .addDependent(drDeleter4).dependsOn(drDeleter3, drDeleter2).build() + .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .build() + .addDependentResource(drDeleter2).dependsOn(drDeleter).build() + .addDependentResource(drDeleter3).dependsOn(drDeleter) + .withDeletePostcondition(noMetDeletePostCondition).build() + .addDependentResource(drDeleter4).dependsOn(drDeleter3, drDeleter2).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -361,10 +369,11 @@ void diamondDeleteErrorInMiddle() { TestDeleterDependent drDeleter3 = new TestDeleterDependent("DR_DELETER_3"); var workflow = new WorkflowBuilder() - .addDependent(drDeleter).withReconcileCondition(not_met_reconcile_condition).build() - .addDependent(drDeleter2).dependsOn(drDeleter).build() - .addDependent(errorDD).dependsOn(drDeleter).build() - .addDependent(drDeleter3).dependsOn(errorDD, drDeleter2).build() + .addDependentResource(drDeleter).withReconcilePrecondition(not_met_reconcile_condition) + .build() + .addDependentResource(drDeleter2).dependsOn(drDeleter).build() + .addDependentResource(errorDD).dependsOn(drDeleter).build() + .addDependentResource(drDeleter3).dependsOn(errorDD, drDeleter2).build() .withThrowExceptionFurther(false) .build(); @@ -382,8 +391,8 @@ void diamondDeleteErrorInMiddle() { @Test void readyConditionTrivialCase() { var workflow = new WorkflowBuilder() - .addDependent(dr1).withReadyCondition(metReadyCondition).build() - .addDependent(dr2).dependsOn(dr1).build() + .addDependentResource(dr1).withReadyPostcondition(metReadyCondition).build() + .addDependentResource(dr2).dependsOn(dr1).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -398,8 +407,8 @@ void readyConditionTrivialCase() { @Test void readyConditionNotMetTrivialCase() { var workflow = new WorkflowBuilder() - .addDependent(dr1).withReadyCondition(notMetReadyCondition).build() - .addDependent(dr2).dependsOn(dr1).build() + .addDependentResource(dr1).withReadyPostcondition(notMetReadyCondition).build() + .addDependentResource(dr2).dependsOn(dr1).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -417,9 +426,9 @@ void readyConditionNotMetInOneParent() { TestDependent dr3 = new TestDependent("DR_3"); var workflow = new WorkflowBuilder() - .addDependent(dr1).withReadyCondition(notMetReadyCondition).build() - .addDependent(dr2).build() - .addDependent(dr3).dependsOn(dr1, dr2).build() + .addDependentResource(dr1).withReadyPostcondition(notMetReadyCondition).build() + .addDependentResource(dr2).build() + .addDependentResource(dr3).dependsOn(dr1, dr2).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); @@ -436,10 +445,11 @@ void diamondShareWithReadyCondition() { TestDependent dr4 = new TestDependent("DR_4"); var workflow = new WorkflowBuilder() - .addDependent(dr1).build() - .addDependent(dr2).dependsOn(dr1).withReadyCondition(notMetReadyCondition).build() - .addDependent(dr3).dependsOn(dr1).build() - .addDependent(dr4).dependsOn(dr2, dr3).build() + .addDependentResource(dr1).build() + .addDependentResource(dr2).dependsOn(dr1).withReadyPostcondition(notMetReadyCondition) + .build() + .addDependentResource(dr3).dependsOn(dr1).build() + .addDependentResource(dr4).dependsOn(dr2, dr3).build() .build(); var res = workflow.reconcile(new TestCustomResource(), null); diff --git a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java index 72a27ed305..01b8bc619c 100644 --- a/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java +++ b/operator-framework-core/src/test/java/io/javaoperatorsdk/operator/processing/dependent/workflow/WorkflowTest.java @@ -10,7 +10,6 @@ import io.javaoperatorsdk.operator.sample.simple.TestCustomResource; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.mock; @SuppressWarnings("rawtypes") @@ -23,9 +22,9 @@ void calculatesTopLevelResources() { var independentDR = mock(DependentResource.class); var workflow = new WorkflowBuilder() - .addDependent(independentDR).build() - .addDependent(dr1).build() - .addDependent(dr2).dependsOn(dr1).build() + .addDependentResource(independentDR).build() + .addDependentResource(dr1).build() + .addDependentResource(dr2).dependsOn(dr1).build() .build(); Set topResources = @@ -43,9 +42,9 @@ void calculatesBottomLevelResources() { var independentDR = mock(DependentResource.class); Workflow workflow = new WorkflowBuilder() - .addDependent(independentDR).build() - .addDependent(dr1).build() - .addDependent(dr2).dependsOn(dr1).build() + .addDependentResource(independentDR).build() + .addDependentResource(dr1).build() + .addDependentResource(dr2).dependsOn(dr1).build() .build(); Set bottomResources = 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 173379f802..b30dc32f8f 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 @@ -58,11 +58,13 @@ void setup() { .thenReturn(DEFAULT_NAMESPACES_SET); when(informerConfiguration.getSecondaryToPrimaryMapper()) .thenReturn(mock(SecondaryToPrimaryMapper.class)); + when(informerConfiguration.getResourceClass()).thenReturn(Deployment.class); informerEventSource = new InformerEventSource<>(informerConfiguration, clientMock); informerEventSource.setTemporalResourceCache(temporaryResourceCacheMock); informerEventSource.setEventHandler(eventHandlerMock); + SecondaryToPrimaryMapper secondaryToPrimaryMapper = mock(SecondaryToPrimaryMapper.class); when(informerConfiguration.getSecondaryToPrimaryMapper()) .thenReturn(secondaryToPrimaryMapper); diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java index 7164dd42af..1a2ff991c2 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/AbstractOperatorExtension.java @@ -161,6 +161,8 @@ protected void afterAllImpl(ExtensionContext context) { } protected void afterEachImpl(ExtensionContext context) { + // resets the config service provider so the controller configs are reconstructed always + ConfigurationServiceProvider.reset(); if (!oneNamespacePerClass) { after(context); } diff --git a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java index 321766d9af..a5266a74e5 100644 --- a/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java +++ b/operator-framework-junit5/src/main/java/io/javaoperatorsdk/operator/junit/LocallyRunOperatorExtension.java @@ -95,6 +95,7 @@ public RegisteredController getRegisteredControllerForReconcile( } @SuppressWarnings("unchecked") + @Override protected void before(ExtensionContext context) { super.before(context); @@ -150,6 +151,7 @@ protected void before(ExtensionContext context) { this.operator.start(); } + @Override protected void after(ExtensionContext context) { super.after(context); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java index dfa97981e7..f1e38ce6eb 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/StandaloneDependentResourceIT.java @@ -16,7 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; -class StandaloneDependentResourceIT { +public class StandaloneDependentResourceIT { public static final String DEPENDENT_TEST_NAME = "dependent-test1"; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowAllFeatureIT.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowAllFeatureIT.java new file mode 100644 index 0000000000..2a8289101f --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/WorkflowAllFeatureIT.java @@ -0,0 +1,125 @@ +package io.javaoperatorsdk.operator; + +import java.time.Duration; +import java.util.HashMap; + +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.apps.Deployment; +import io.javaoperatorsdk.operator.junit.LocallyRunOperatorExtension; +import io.javaoperatorsdk.operator.sample.workflowallfeature.WorkflowAllFeatureCustomResource; +import io.javaoperatorsdk.operator.sample.workflowallfeature.WorkflowAllFeatureReconciler; +import io.javaoperatorsdk.operator.sample.workflowallfeature.WorkflowAllFeatureSpec; + +import static io.javaoperatorsdk.operator.sample.workflowallfeature.ConfigMapDependentResource.READY_TO_DELETE_ANNOTATION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class WorkflowAllFeatureIT { + + public static final String RESOURCE_NAME = "test"; + private static final Duration ONE_MINUTE = Duration.ofMinutes(1); + + @RegisterExtension + LocallyRunOperatorExtension operator = + LocallyRunOperatorExtension.builder().withReconciler(WorkflowAllFeatureReconciler.class) + .build(); + + @Test + void configMapNotReconciledUntilDeploymentReady() { + operator.create(WorkflowAllFeatureCustomResource.class, customResource(true)); + await().untilAsserted( + () -> { + assertThat(operator + .getReconcilerOfType(WorkflowAllFeatureReconciler.class) + .getNumberOfReconciliationExecution()) + .isPositive(); + assertThat(operator.get(Deployment.class, RESOURCE_NAME)).isNotNull(); + assertThat(operator.get(ConfigMap.class, RESOURCE_NAME)).isNull(); + }); + + await().atMost(ONE_MINUTE).untilAsserted(() -> { + assertThat(operator + .getReconcilerOfType(WorkflowAllFeatureReconciler.class) + .getNumberOfReconciliationExecution()) + .isGreaterThan(1); + assertThat(operator.get(ConfigMap.class, RESOURCE_NAME)).isNotNull(); + assertThat(operator.get(WorkflowAllFeatureCustomResource.class, RESOURCE_NAME) + .getStatus().getReady()).isTrue(); + }); + + markConfigMapForDelete(); + } + + + @Test + void configMapNotReconciledIfReconcileConditionNotMet() { + var resource = operator.create(WorkflowAllFeatureCustomResource.class, customResource(false)); + + await().atMost(ONE_MINUTE).untilAsserted(() -> { + assertThat(operator.get(ConfigMap.class, RESOURCE_NAME)).isNull(); + assertThat(operator.get(WorkflowAllFeatureCustomResource.class, RESOURCE_NAME) + .getStatus().getReady()).isTrue(); + }); + + resource.getSpec().setCreateConfigMap(true); + operator.replace(WorkflowAllFeatureCustomResource.class, resource); + + await().untilAsserted(() -> { + assertThat(operator.get(ConfigMap.class, RESOURCE_NAME)).isNotNull(); + assertThat(operator.get(WorkflowAllFeatureCustomResource.class, RESOURCE_NAME) + .getStatus().getReady()).isTrue(); + }); + } + + + @Test + void configMapNotDeletedUntilNotMarked() { + var resource = operator.create(WorkflowAllFeatureCustomResource.class, customResource(true)); + + await().atMost(ONE_MINUTE).untilAsserted(() -> { + assertThat(operator.get(WorkflowAllFeatureCustomResource.class, RESOURCE_NAME).getStatus()) + .isNotNull(); + assertThat(operator.get(WorkflowAllFeatureCustomResource.class, RESOURCE_NAME) + .getStatus().getReady()).isTrue(); + assertThat(operator.get(ConfigMap.class, RESOURCE_NAME)).isNotNull(); + }); + + operator.delete(WorkflowAllFeatureCustomResource.class, resource); + + await().pollDelay(Duration.ofMillis(300)).untilAsserted(() -> { + assertThat(operator.get(ConfigMap.class, RESOURCE_NAME)).isNotNull(); + assertThat(operator.get(WorkflowAllFeatureCustomResource.class, RESOURCE_NAME)).isNotNull(); + }); + + markConfigMapForDelete(); + + await().atMost(ONE_MINUTE).untilAsserted(() -> { + assertThat(operator.get(ConfigMap.class, RESOURCE_NAME)).isNull(); + assertThat(operator.get(WorkflowAllFeatureCustomResource.class, RESOURCE_NAME)).isNull(); + }); + } + + private void markConfigMapForDelete() { + var cm = operator.get(ConfigMap.class, RESOURCE_NAME); + if (cm.getMetadata().getAnnotations() == null) { + cm.getMetadata().setAnnotations(new HashMap<>()); + } + cm.getMetadata().getAnnotations().put(READY_TO_DELETE_ANNOTATION, "true"); + operator.replace(ConfigMap.class, cm); + } + + private WorkflowAllFeatureCustomResource customResource(boolean createConfigMap) { + var res = new WorkflowAllFeatureCustomResource(); + res.setMetadata(new ObjectMetaBuilder() + .withName(RESOURCE_NAME) + .build()); + res.setSpec(new WorkflowAllFeatureSpec()); + res.getSpec().setCreateConfigMap(createConfigMap); + return res; + } + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperReconciler.java index 2608b8373c..ec4a2c86b9 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentannotationsecondarymapper/DependentAnnotationSecondaryMapperReconciler.java @@ -13,8 +13,8 @@ import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.support.TestExecutionInfoProvider; -@ControllerConfiguration(dependents = {@Dependent( - type = DependentAnnotationSecondaryMapperReconciler.ConfigMapDependentResource.class)}) +@ControllerConfiguration(dependents = @Dependent( + type = DependentAnnotationSecondaryMapperReconciler.ConfigMapDependentResource.class)) public class DependentAnnotationSecondaryMapperReconciler implements Reconciler, TestExecutionInfoProvider { diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentresourcecrossref/DependentResourceCrossRefReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentresourcecrossref/DependentResourceCrossRefReconciler.java index d6eedc26dc..bb319741b3 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentresourcecrossref/DependentResourceCrossRefReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/dependentresourcecrossref/DependentResourceCrossRefReconciler.java @@ -12,13 +12,18 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDKubernetesDependentResource; +import static io.javaoperatorsdk.operator.sample.dependentresourcecrossref.DependentResourceCrossRefReconciler.SECRET_NAME; + @ControllerConfiguration(dependents = { - @Dependent(type = DependentResourceCrossRefReconciler.SecretDependentResource.class), - @Dependent(type = DependentResourceCrossRefReconciler.ConfigMapDependentResource.class)}) + @Dependent(name = SECRET_NAME, + type = DependentResourceCrossRefReconciler.SecretDependentResource.class), + @Dependent(type = DependentResourceCrossRefReconciler.ConfigMapDependentResource.class, + dependsOn = SECRET_NAME)}) public class DependentResourceCrossRefReconciler implements Reconciler, ErrorStatusHandler { + public static final String SECRET_NAME = "secret"; private final AtomicInteger numberOfExecutions = new AtomicInteger(0); private volatile boolean errorHappened = false; diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/OrderedManagedDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/OrderedManagedDependentTestReconciler.java index 8ca8f0651d..f7172ca44d 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/OrderedManagedDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/orderedmanageddependent/OrderedManagedDependentTestReconciler.java @@ -16,8 +16,8 @@ @ControllerConfiguration( namespaces = Constants.WATCH_CURRENT_NAMESPACE, dependents = { - @Dependent(type = ConfigMapDependentResource1.class), - @Dependent(type = ConfigMapDependentResource2.class) + @Dependent(type = ConfigMapDependentResource1.class, name = "cm1"), + @Dependent(type = ConfigMapDependentResource2.class, dependsOn = "cm1") }) public class OrderedManagedDependentTestReconciler implements Reconciler, diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java index 4853a22e9a..af6b2d7e25 100644 --- a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/standalonedependent/StandaloneDependentTestReconciler.java @@ -7,6 +7,7 @@ import io.fabric8.kubernetes.client.KubernetesClient; import io.fabric8.kubernetes.client.KubernetesClientException; import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.StandaloneDependentResourceIT; import io.javaoperatorsdk.operator.api.reconciler.Context; import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusHandler; @@ -96,7 +97,8 @@ public DeploymentDependentResource() { protected Deployment desired(StandaloneDependentTestCustomResource primary, Context context) { Deployment deployment = - ReconcilerUtils.loadYaml(Deployment.class, getClass(), "nginx-deployment.yaml"); + ReconcilerUtils.loadYaml(Deployment.class, StandaloneDependentResourceIT.class, + "nginx-deployment.yaml"); deployment.getMetadata().setName(primary.getMetadata().getName()); deployment.getSpec().setReplicas(primary.getSpec().getReplicaCount()); deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/ConfigMapDeletePostCondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/ConfigMapDeletePostCondition.java new file mode 100644 index 0000000000..c5c908dfe6 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/ConfigMapDeletePostCondition.java @@ -0,0 +1,24 @@ +package io.javaoperatorsdk.operator.sample.workflowallfeature; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.fabric8.kubernetes.api.model.ConfigMap; +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 ConfigMapDeletePostCondition + implements Condition { + + private static final Logger log = LoggerFactory.getLogger(ConfigMapDeletePostCondition.class); + + @Override + public boolean isMet( + DependentResource dependentResource, + WorkflowAllFeatureCustomResource primary, Context context) { + var configMapDeleted = dependentResource.getSecondaryResource(primary).isEmpty(); + log.debug("Config Map Deleted: {}", configMapDeleted); + return configMapDeleted; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/ConfigMapDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/ConfigMapDependentResource.java new file mode 100644 index 0000000000..0620d6a753 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/ConfigMapDependentResource.java @@ -0,0 +1,59 @@ +package io.javaoperatorsdk.operator.sample.workflowallfeature; + +import java.util.Map; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.api.reconciler.dependent.Deleter; +import io.javaoperatorsdk.operator.processing.dependent.Creator; +import io.javaoperatorsdk.operator.processing.dependent.Updater; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; +import io.javaoperatorsdk.operator.processing.event.ResourceID; + +public class ConfigMapDependentResource + extends KubernetesDependentResource + implements Creator, + Updater, + Deleter { + + public static final String READY_TO_DELETE_ANNOTATION = "ready-to-delete"; + + private static final Logger log = LoggerFactory.getLogger(ConfigMapDependentResource.class); + + public ConfigMapDependentResource() { + super(ConfigMap.class); + } + + @Override + protected ConfigMap desired(WorkflowAllFeatureCustomResource primary, + Context context) { + ConfigMap configMap = new ConfigMap(); + configMap.setMetadata(new ObjectMetaBuilder() + .withName(primary.getMetadata().getName()) + .withNamespace(primary.getMetadata().getNamespace()) + .build()); + configMap.setData(Map.of("key", "data")); + return configMap; + } + + @Override + public void delete(WorkflowAllFeatureCustomResource primary, + Context context) { + Optional optionalConfigMap = context.getSecondaryResource(ConfigMap.class); + if (optionalConfigMap.isEmpty()) { + log.debug("Config Map not found for primary: {}", ResourceID.fromResource(primary)); + return; + } + optionalConfigMap.ifPresent((configMap -> { + if (configMap.getMetadata().getAnnotations() != null + && configMap.getMetadata().getAnnotations().get(READY_TO_DELETE_ANNOTATION) != null) { + client.resource(configMap).delete(); + } + })); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/ConfigMapReconcileCondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/ConfigMapReconcileCondition.java new file mode 100644 index 0000000000..65409efc36 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/ConfigMapReconcileCondition.java @@ -0,0 +1,17 @@ +package io.javaoperatorsdk.operator.sample.workflowallfeature; + +import io.fabric8.kubernetes.api.model.ConfigMap; +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 ConfigMapReconcileCondition + implements Condition { + + @Override + public boolean isMet( + DependentResource dependentResource, + WorkflowAllFeatureCustomResource primary, Context context) { + return primary.getSpec().isCreateConfigMap(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/DeploymentDependentResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/DeploymentDependentResource.java new file mode 100644 index 0000000000..61cf18f57b --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/DeploymentDependentResource.java @@ -0,0 +1,29 @@ +package io.javaoperatorsdk.operator.sample.workflowallfeature; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +import io.javaoperatorsdk.operator.ReconcilerUtils; +import io.javaoperatorsdk.operator.WorkflowAllFeatureIT; +import io.javaoperatorsdk.operator.api.reconciler.Context; +import io.javaoperatorsdk.operator.processing.dependent.kubernetes.CRUDNoGCKubernetesDependentResource; + +public class DeploymentDependentResource extends + CRUDNoGCKubernetesDependentResource { + + public DeploymentDependentResource() { + super(Deployment.class); + } + + @Override + protected Deployment desired(WorkflowAllFeatureCustomResource primary, + Context context) { + Deployment deployment = + ReconcilerUtils.loadYaml(Deployment.class, WorkflowAllFeatureIT.class, + "nginx-deployment.yaml"); + deployment.getMetadata().setName(primary.getMetadata().getName()); + deployment.getSpec().setReplicas(2); + deployment.getMetadata().setNamespace(primary.getMetadata().getNamespace()); + return deployment; + } +} + + diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/DeploymentReadyCondition.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/DeploymentReadyCondition.java new file mode 100644 index 0000000000..c8646f6ea5 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/DeploymentReadyCondition.java @@ -0,0 +1,20 @@ +package io.javaoperatorsdk.operator.sample.workflowallfeature; + +import io.fabric8.kubernetes.api.model.apps.Deployment; +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 DeploymentReadyCondition + implements Condition { + @Override + public boolean isMet( + DependentResource dependentResource, + WorkflowAllFeatureCustomResource primary, Context context) { + + var deployment = dependentResource.getSecondaryResource(primary).orElseThrow(); + var readyReplicas = deployment.getStatus().getReadyReplicas(); + + return readyReplicas != null && deployment.getSpec().getReplicas().equals(readyReplicas); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureCustomResource.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureCustomResource.java new file mode 100644 index 0000000000..ee764f4681 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureCustomResource.java @@ -0,0 +1,18 @@ +package io.javaoperatorsdk.operator.sample.workflowallfeature; + +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("waf") +public class WorkflowAllFeatureCustomResource + extends CustomResource + implements Namespaced { + + + +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureReconciler.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureReconciler.java new file mode 100644 index 0000000000..2c25d13924 --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureReconciler.java @@ -0,0 +1,57 @@ +package io.javaoperatorsdk.operator.sample.workflowallfeature; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.javaoperatorsdk.operator.api.reconciler.*; +import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; + +import static io.javaoperatorsdk.operator.sample.workflowallfeature.WorkflowAllFeatureReconciler.DEPLOYMENT_NAME; + +@ControllerConfiguration(dependents = { + @Dependent(name = DEPLOYMENT_NAME, type = DeploymentDependentResource.class, + readyPostcondition = DeploymentReadyCondition.class), + @Dependent(type = ConfigMapDependentResource.class, + reconcilePrecondition = ConfigMapReconcileCondition.class, + deletePostcondition = ConfigMapDeletePostCondition.class, + dependsOn = DEPLOYMENT_NAME) +}) +public class WorkflowAllFeatureReconciler + implements Reconciler, + Cleaner { + + public static final String DEPLOYMENT_NAME = "deployment"; + + private final AtomicInteger numberOfReconciliationExecution = new AtomicInteger(0); + private final AtomicInteger numberOfCleanupExecution = new AtomicInteger(0); + + @Override + public UpdateControl reconcile( + WorkflowAllFeatureCustomResource resource, + Context context) { + numberOfReconciliationExecution.addAndGet(1); + if (resource.getStatus() == null) { + resource.setStatus(new WorkflowAllFeatureStatus()); + } + resource.getStatus() + .setReady( + context.managedDependentResourceContext() + .getWorkflowReconcileResult().orElseThrow() + .allDependentResourcesReady()); + return UpdateControl.patchStatus(resource); + } + + public int getNumberOfReconciliationExecution() { + return numberOfReconciliationExecution.get(); + } + + public int getNumberOfCleanupExecution() { + return numberOfCleanupExecution.get(); + } + + @Override + public DeleteControl cleanup(WorkflowAllFeatureCustomResource resource, + Context context) { + numberOfCleanupExecution.addAndGet(1); + return DeleteControl.defaultDelete(); + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureSpec.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureSpec.java new file mode 100644 index 0000000000..6d5cfd7a5a --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureSpec.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.workflowallfeature; + +public class WorkflowAllFeatureSpec { + + private boolean createConfigMap = false; + + public boolean isCreateConfigMap() { + return createConfigMap; + } + + public WorkflowAllFeatureSpec setCreateConfigMap(boolean createConfigMap) { + this.createConfigMap = createConfigMap; + return this; + } +} diff --git a/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureStatus.java b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureStatus.java new file mode 100644 index 0000000000..11d0798fca --- /dev/null +++ b/operator-framework/src/test/java/io/javaoperatorsdk/operator/sample/workflowallfeature/WorkflowAllFeatureStatus.java @@ -0,0 +1,15 @@ +package io.javaoperatorsdk.operator.sample.workflowallfeature; + +public class WorkflowAllFeatureStatus { + + private Boolean ready; + + public Boolean getReady() { + return ready; + } + + public WorkflowAllFeatureStatus setReady(Boolean ready) { + this.ready = ready; + return this; + } +} diff --git a/operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/standalonedependent/nginx-deployment.yaml b/operator-framework/src/test/resources/io/javaoperatorsdk/operator/nginx-deployment.yaml similarity index 100% rename from operator-framework/src/test/resources/io/javaoperatorsdk/operator/sample/standalonedependent/nginx-deployment.yaml rename to operator-framework/src/test/resources/io/javaoperatorsdk/operator/nginx-deployment.yaml diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java index 08b65162fd..05187aea2b 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/MySQLSchemaReconciler.java @@ -21,8 +21,9 @@ @ControllerConfiguration( dependents = { - @Dependent(type = SecretDependentResource.class), - @Dependent(type = SchemaDependentResource.class, name = SchemaDependentResource.NAME) + @Dependent(type = SecretDependentResource.class, name = SecretDependentResource.NAME), + @Dependent(type = SchemaDependentResource.class, name = SchemaDependentResource.NAME, + dependsOn = SecretDependentResource.NAME) }) public class MySQLSchemaReconciler implements Reconciler, ErrorStatusHandler { diff --git a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java index 043b50a6cc..1aa2ad62e5 100644 --- a/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java +++ b/sample-operators/mysql-schema/src/main/java/io/javaoperatorsdk/operator/sample/dependent/SecretDependentResource.java @@ -17,7 +17,7 @@ public class SecretDependentResource extends KubernetesDependentResource implements Creator, SecondaryToPrimaryMapper { - + public static final String NAME = "secret"; public static final String SECRET_SUFFIX = "-secret"; public static final String SECRET_FORMAT = "%s" + SECRET_SUFFIX; public static final String USERNAME_FORMAT = "%s-user"; diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ExposedIngressCondition.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ExposedIngressCondition.java new file mode 100644 index 0000000000..218d1f8ca2 --- /dev/null +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/ExposedIngressCondition.java @@ -0,0 +1,14 @@ +package io.javaoperatorsdk.operator.sample; + +import io.fabric8.kubernetes.api.model.networking.v1.Ingress; +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 ExposedIngressCondition implements Condition { + @Override + public boolean isMet(DependentResource dependentResource, WebPage primary, + Context context) { + return primary.getSpec().getExposed(); + } +} diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java index 2ced5a0f3d..e9c5218cf8 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageDependentsWorkflowReconciler.java @@ -3,19 +3,14 @@ import java.util.Arrays; import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.fabric8.kubernetes.api.model.ConfigMap; import io.fabric8.kubernetes.api.model.Service; import io.fabric8.kubernetes.api.model.apps.Deployment; import io.fabric8.kubernetes.api.model.networking.v1.Ingress; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.reconciler.*; -import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResource; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; -import io.javaoperatorsdk.operator.processing.dependent.workflow.Condition; import io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow; import io.javaoperatorsdk.operator.processing.dependent.workflow.builder.WorkflowBuilder; import io.javaoperatorsdk.operator.processing.event.source.EventSource; @@ -31,8 +26,6 @@ public class WebPageDependentsWorkflowReconciler implements Reconciler, ErrorStatusHandler, EventSourceInitializer { public static final String DEPENDENT_RESOURCE_LABEL_SELECTOR = "!low-level"; - private static final Logger log = - LoggerFactory.getLogger(WebPageDependentsWorkflowReconciler.class); private KubernetesDependentResource configMapDR; private KubernetesDependentResource deploymentDR; @@ -44,10 +37,11 @@ public class WebPageDependentsWorkflowReconciler public WebPageDependentsWorkflowReconciler(KubernetesClient kubernetesClient) { initDependentResources(kubernetesClient); workflow = new WorkflowBuilder() - .addDependent(configMapDR).build() - .addDependent(deploymentDR).build() - .addDependent(serviceDR).build() - .addDependent(ingressDR).withReconcileCondition(new IngressCondition()).build() + .addDependentResource(configMapDR).build() + .addDependentResource(deploymentDR).build() + .addDependentResource(serviceDR).build() + .addDependentResource(ingressDR).withReconcilePrecondition(new ExposedIngressCondition()) + .build() .build(); } @@ -90,12 +84,6 @@ private void initDependentResources(KubernetesClient client) { }); } - static class IngressCondition implements Condition { - @Override - public boolean isMet(DependentResource dependentResource, WebPage primary, - Context context) { - return primary.getSpec().getExposed(); - } - } + } diff --git a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java index ac89ebd269..b2e0963ef2 100644 --- a/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java +++ b/sample-operators/webpage/src/main/java/io/javaoperatorsdk/operator/sample/WebPageManagedDependentsReconciler.java @@ -20,7 +20,9 @@ dependents = { @Dependent(type = ConfigMapDependentResource.class), @Dependent(type = DeploymentDependentResource.class), - @Dependent(type = ServiceDependentResource.class) + @Dependent(type = ServiceDependentResource.class), + @Dependent(type = IngressDependentResource.class, + reconcilePrecondition = ExposedIngressCondition.class) }) public class WebPageManagedDependentsReconciler implements Reconciler, ErrorStatusHandler {