From b54d31bb8fe624732d1088424cb543ee9e21626e Mon Sep 17 00:00:00 2001 From: Chris Laprun Date: Wed, 20 Mar 2024 15:59:10 +0100 Subject: [PATCH] feat: adapt code to Workflow now being a separate annotation Signed-off-by: Chris Laprun --- .../bundle/sources/ThirdReconciler.java | 8 ++- .../operatorsdk/common/Constants.java | 2 + .../common/ReconcilerAugmentedClassInfo.java | 11 ++- .../deployment/AddClusterRolesDecorator.java | 3 +- ...arkusControllerConfigurationBuildStep.java | 68 ++++++++++--------- .../test/sources/TestReconciler.java | 4 +- .../runtime/QuarkusConfigurationService.java | 19 +++--- .../QuarkusControllerConfiguration.java | 52 ++++++++------ .../runtime/QuarkusManagedWorkflow.java | 32 +++++++-- .../runtime/QuarkusWorkflowSpec.java | 37 ++++++++++ .../runtime/devconsole/ControllerInfo.java | 5 +- .../it/AnnotatedDependentReconciler.java | 4 +- .../it/DependentDefiningReconciler.java | 4 +- .../operatorsdk/it/OperatorSDKResource.java | 5 +- .../it/SetOperatorLevelNamespacesTest.java | 2 +- .../java/io/halkyon/DeploymentDependent.java | 2 +- .../java/io/halkyon/ExposedAppReconciler.java | 7 +- .../java/io/halkyon/IngressDependent.java | 2 +- .../java/io/halkyon/ServiceDependent.java | 2 +- .../mysqlschema/MySQLSchemaReconciler.java | 22 +++--- 20 files changed, 196 insertions(+), 95 deletions(-) create mode 100644 core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusWorkflowSpec.java diff --git a/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/sources/ThirdReconciler.java b/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/sources/ThirdReconciler.java index 0914e317..354a953b 100644 --- a/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/sources/ThirdReconciler.java +++ b/bundle-generator/deployment/src/test/java/io/quarkiverse/operatorsdk/bundle/sources/ThirdReconciler.java @@ -4,18 +4,20 @@ import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Workflow; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.quarkiverse.operatorsdk.annotations.CSVMetadata; import io.quarkiverse.operatorsdk.annotations.CSVMetadata.Annotations; import io.quarkiverse.operatorsdk.annotations.CSVMetadata.Annotations.Annotation; import io.quarkiverse.operatorsdk.annotations.CSVMetadata.RequiredCRD; -@CSVMetadata(name = "third-operator", requiredCRDs = @RequiredCRD(kind = SecondExternal.KIND, name = "externalagains." - + SecondExternal.GROUP, version = SecondExternal.VERSION), replaces = "1.0.0", annotations = @Annotations(skipRange = ">=1.0.0 <1.0.3", capabilities = "Test", others = @Annotation(name = "foo", value = "bar"))) -@ControllerConfiguration(name = ThirdReconciler.NAME, dependents = { +@Workflow(dependents = { @Dependent(type = ExternalDependentResource.class), @Dependent(type = PodDependentResource.class) }) +@CSVMetadata(bundleName = "third-operator", requiredCRDs = @RequiredCRD(kind = SecondExternal.KIND, name = "externalagains." + + SecondExternal.GROUP, version = SecondExternal.VERSION), replaces = "1.0.0", annotations = @Annotations(skipRange = ">=1.0.0 <1.0.3", capabilities = "Test", others = @Annotation(name = "foo", value = "bar"))) +@ControllerConfiguration(name = ThirdReconciler.NAME) public class ThirdReconciler implements Reconciler { public static final String NAME = "third"; diff --git a/common-deployment/src/main/java/io/quarkiverse/operatorsdk/common/Constants.java b/common-deployment/src/main/java/io/quarkiverse/operatorsdk/common/Constants.java index a9631dd2..f33b2a47 100644 --- a/common-deployment/src/main/java/io/quarkiverse/operatorsdk/common/Constants.java +++ b/common-deployment/src/main/java/io/quarkiverse/operatorsdk/common/Constants.java @@ -9,6 +9,7 @@ import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Ignore; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; +import io.javaoperatorsdk.operator.api.reconciler.Workflow; import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource; import io.quarkiverse.operatorsdk.annotations.AdditionalRBACRules; import io.quarkiverse.operatorsdk.annotations.RBACRule; @@ -22,6 +23,7 @@ private Constants() { public static final DotName CUSTOM_RESOURCE = DotName.createSimple(CustomResource.class.getName()); public static final DotName HAS_METADATA = DotName.createSimple(HasMetadata.class.getName()); public static final DotName CONTROLLER_CONFIGURATION = DotName.createSimple(ControllerConfiguration.class.getName()); + public static final DotName WORKFLOW = DotName.createSimple(Workflow.class.getName()); public static final DotName DEPENDENT_RESOURCE = DotName.createSimple(DependentResource.class.getName()); public static final DotName CONFIGURED = DotName.createSimple(Configured.class.getName()); public static final DotName ANNOTATION_CONFIGURABLE = DotName.createSimple(AnnotationConfigurable.class.getName()); diff --git a/common-deployment/src/main/java/io/quarkiverse/operatorsdk/common/ReconcilerAugmentedClassInfo.java b/common-deployment/src/main/java/io/quarkiverse/operatorsdk/common/ReconcilerAugmentedClassInfo.java index 6f29133c..16e180ac 100644 --- a/common-deployment/src/main/java/io/quarkiverse/operatorsdk/common/ReconcilerAugmentedClassInfo.java +++ b/common-deployment/src/main/java/io/quarkiverse/operatorsdk/common/ReconcilerAugmentedClassInfo.java @@ -1,9 +1,6 @@ package io.quarkiverse.operatorsdk.common; -import static io.quarkiverse.operatorsdk.common.Constants.CONTROLLER_CONFIGURATION; -import static io.quarkiverse.operatorsdk.common.Constants.CUSTOM_RESOURCE; -import static io.quarkiverse.operatorsdk.common.Constants.HAS_METADATA; -import static io.quarkiverse.operatorsdk.common.Constants.RECONCILER; +import static io.quarkiverse.operatorsdk.common.Constants.*; import java.util.Collection; import java.util.Collections; @@ -48,10 +45,10 @@ protected void doAugment(IndexView index, Logger log, Map contex // extract dependent information final var reconciler = classInfo(); - final var controllerAnnotation = reconciler.declaredAnnotation(CONTROLLER_CONFIGURATION); + final var workflow = reconciler.declaredAnnotation(WORKFLOW); dependentResourceInfos = Collections.emptyList(); - if (controllerAnnotation != null) { - final var dependents = controllerAnnotation.value("dependents"); + if (workflow != null) { + final var dependents = workflow.value("dependents"); if (dependents != null) { final var dependentAnnotations = dependents.asNestedArray(); var dependentResources = Collections. emptyMap(); diff --git a/core/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/AddClusterRolesDecorator.java b/core/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/AddClusterRolesDecorator.java index 416ca921..27ded6c2 100644 --- a/core/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/AddClusterRolesDecorator.java +++ b/core/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/AddClusterRolesDecorator.java @@ -77,7 +77,8 @@ public static ClusterRole createClusterRole(QuarkusControllerConfiguration cr .endMetadata() .addToRules(rule.build()); - final Map> dependentsMetadata = cri.getDependentsMetadata(); + @SuppressWarnings("rawtypes") + final Map dependentsMetadata = cri.dependentsMetadata(); dependentsMetadata.forEach((name, spec) -> { final var dependentResourceClass = spec.getDependentResourceClass(); final var associatedResourceClass = spec.getDependentType(); diff --git a/core/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/QuarkusControllerConfigurationBuildStep.java b/core/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/QuarkusControllerConfigurationBuildStep.java index ab9d3b8b..e4dedca5 100644 --- a/core/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/QuarkusControllerConfigurationBuildStep.java +++ b/core/deployment/src/main/java/io/quarkiverse/operatorsdk/deployment/QuarkusControllerConfigurationBuildStep.java @@ -25,8 +25,7 @@ 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.ManagedWorkflow; -import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowFactory; +import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflowSupport; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter; @@ -52,6 +51,7 @@ class QuarkusControllerConfigurationBuildStep { static final Logger log = Logger.getLogger(QuarkusControllerConfigurationBuildStep.class.getName()); + private static final ManagedWorkflowSupport workflowSupport = new ManagedWorkflowSupport(); private static final KubernetesDependentConverter KUBERNETES_DEPENDENT_CONVERTER = new KubernetesDependentConverter() { @Override @@ -235,15 +235,7 @@ static QuarkusControllerConfiguration createConfiguration( final var primaryAsResource = primaryInfo.asResourceTargeting(); final var resourceClass = primaryInfo.loadAssociatedClass(); final String resourceFullName = primaryAsResource.fullResourceName(); - // initialize dependent specs - final Map dependentResources; - final var dependentResourceInfos = reconcilerInfo.getDependentResourceInfos(); - final var hasDependents = !dependentResourceInfos.isEmpty(); - if (hasDependents) { - dependentResources = new HashMap<>(dependentResourceInfos.size()); - } else { - dependentResources = Collections.emptyMap(); - } + configuration = new QuarkusControllerConfiguration( reconcilerClassName, name, @@ -259,27 +251,14 @@ static QuarkusControllerConfiguration createConfiguration( primaryAsResource.hasNonVoidStatus(), maxReconciliationInterval, onAddFilter, onUpdateFilter, genericFilter, retryClass, retryConfigurationClass, rateLimiterClass, - rateLimiterConfigurationClass, dependentResources, null, additionalRBACRules, fieldManager, itemStore); - - if (hasDependents) { - dependentResourceInfos.forEach(dependent -> { - final var spec = createDependentResourceSpec(dependent, index, configuration); - final var dependentName = dependent.classInfo().name(); - dependentResources.put(dependentName.toString(), spec); - }); - } + rateLimiterConfigurationClass, additionalRBACRules, fieldManager, itemStore); - // compute workflow and set it (originally set to null in constructor) - final ManagedWorkflow workflow; - if (hasDependents) { - // make workflow bytecode serializable - final var original = ManagedWorkflowFactory.DEFAULT.workflowFor(configuration); - workflow = new QuarkusManagedWorkflow<>(original.getOrderedSpecs(), - original.hasCleaner()); - } else { - workflow = QuarkusManagedWorkflow.noOpManagedWorkflow; - } - configuration.setWorkflow(workflow); + // compute workflow and set it + initializeWorkflowIfNeeded(configuration, reconcilerInfo, index); + + // need to set the namespaces after the dependents have been set so that they can be properly updated if needed + // however, we need to do it in a way that doesn't reset whether the namespaces were set by the user or not + configuration.propagateNamespacesToDependents(); log.infov( "Processed ''{0}'' reconciler named ''{1}'' for ''{2}'' resource (version ''{3}'')", @@ -287,6 +266,33 @@ static QuarkusControllerConfiguration createConfiguration( return configuration; } + private static void initializeWorkflowIfNeeded(QuarkusControllerConfiguration configuration, + ReconcilerAugmentedClassInfo reconcilerInfo, IndexView index) { + final var workflowAnnotation = reconcilerInfo.classInfo().declaredAnnotation(WORKFLOW); + @SuppressWarnings("unchecked") + QuarkusManagedWorkflow workflow = QuarkusManagedWorkflow.noOpManagedWorkflow; + if (workflowAnnotation != null) { + final var dependentResourceInfos = reconcilerInfo.getDependentResourceInfos(); + if (!dependentResourceInfos.isEmpty()) { + Map dependentResources = new HashMap<>(dependentResourceInfos.size()); + dependentResourceInfos.forEach(dependent -> { + final var spec = createDependentResourceSpec(dependent, index, configuration); + final var dependentName = dependent.classInfo().name(); + dependentResources.put(dependentName.toString(), spec); + }); + + final var explicitInvocation = ConfigurationUtils.annotationValueOrDefault( + workflowAnnotation, "explicitInvocation", AnnotationValue::asBoolean, + () -> false); + // make workflow bytecode serializable + final var spec = new QuarkusWorkflowSpec(dependentResources, explicitInvocation); + final var original = workflowSupport.createWorkflow(spec); + workflow = new QuarkusManagedWorkflow<>(spec, original.getOrderedSpecs(), original.hasCleaner()); + } + } + configuration.setWorkflow(workflow); + } + private static List extractAdditionalRBACRules(ClassInfo info) { // if there are multiple annotations they should be found under an automatically generated AdditionalRBACRules final var additionalRuleAnnotations = ConfigurationUtils.annotationValueOrDefault( diff --git a/core/deployment/src/test/java/io/quarkiverse/operatorsdk/test/sources/TestReconciler.java b/core/deployment/src/test/java/io/quarkiverse/operatorsdk/test/sources/TestReconciler.java index 0217ed52..d02e6ec8 100644 --- a/core/deployment/src/test/java/io/quarkiverse/operatorsdk/test/sources/TestReconciler.java +++ b/core/deployment/src/test/java/io/quarkiverse/operatorsdk/test/sources/TestReconciler.java @@ -4,16 +4,18 @@ import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Workflow; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.quarkiverse.operatorsdk.annotations.RBACRule; import io.quarkiverse.operatorsdk.annotations.RBACVerbs; -@ControllerConfiguration(name = TestReconciler.NAME, dependents = { +@Workflow(dependents = { @Dependent(type = CRUDConfigMap.class), @Dependent(type = ReadOnlySecret.class), @Dependent(type = CreateOnlyService.class), @Dependent(type = NonKubeResource.class) }) +@ControllerConfiguration(name = TestReconciler.NAME) @RBACRule(verbs = RBACVerbs.UPDATE, apiGroups = RBACRule.ALL, resources = RBACRule.ALL) public class TestReconciler implements Reconciler { diff --git a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusConfigurationService.java b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusConfigurationService.java index fa782075..34a34ab4 100644 --- a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusConfigurationService.java +++ b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusConfigurationService.java @@ -244,15 +244,18 @@ private static String getDependentKeyFromNames(String controllerName, String dep return controllerName + "#" + dependentName; } - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings({ "rawtypes" }) public DependentResourceSpecMetadata getDependentByName(String controllerName, String dependentName) { - return (DependentResourceSpecMetadata) controllerConfigurations() - .filter(cc -> controllerName.equals(cc.getName())) - .findFirst() - .flatMap(cc -> cc.getDependentResources().stream() - .filter(drs -> dependentName.equals(((DependentResourceSpec) drs).getName())) - .findFirst()) - .orElse(null); + final ControllerConfiguration cc = getFor(controllerName); + if (cc == null) { + return null; + } else { + return cc.getWorkflowSpec().flatMap(spec -> spec.getDependentResourceSpecs().stream() + .filter(r -> r.getName().equals(dependentName) && r instanceof DependentResourceSpecMetadata) + .map(DependentResourceSpecMetadata.class::cast) + .findFirst()) + .orElse(null); + } } @SuppressWarnings("rawtypes") diff --git a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusControllerConfiguration.java b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusControllerConfiguration.java index 24796c23..f8b80014 100644 --- a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusControllerConfiguration.java +++ b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusControllerConfiguration.java @@ -14,8 +14,8 @@ import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationProvider; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; -import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow; import io.javaoperatorsdk.operator.processing.event.rate.LinearRateLimiter; import io.javaoperatorsdk.operator.processing.event.rate.RateLimiter; import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter; @@ -68,10 +68,9 @@ public DefaultRateLimiter(Duration refreshPeriod, int limitForPeriod) { private Set namespaces; private boolean wereNamespacesSet; private String labelSelector; - private final Map> dependentsMetadata; private Retry retry; private RateLimiter rateLimiter; - private ManagedWorkflow workflow; + private QuarkusManagedWorkflow workflow; private QuarkusConfigurationService parent; private ExternalGradualRetryConfiguration gradualRetry; @@ -92,7 +91,6 @@ public QuarkusControllerConfiguration( OnAddFilter onAddFilter, OnUpdateFilter onUpdateFilter, GenericFilter genericFilter, Class retryClass, Class retryConfigurationClass, Class rateLimiterClass, Class rateLimiterConfigurationClass, - Map> dependentsMetadata, ManagedWorkflow workflow, List additionalRBACRules, String fieldManager, ItemStore nullableItemStore) { this.associatedReconcilerClassName = associatedReconcilerClassName; this.name = name; @@ -102,8 +100,6 @@ public QuarkusControllerConfiguration( this.resourceClass = resourceClass; this.informerListLimit = Optional.ofNullable(nullableInformerListLimit); this.additionalRBACRules = additionalRBACRules; - this.dependentsMetadata = dependentsMetadata; - this.workflow = workflow; setNamespaces(namespaces); this.wereNamespacesSet = wereNamespacesSet; setFinalizer(finalizerName); @@ -194,17 +190,34 @@ public Set getNamespaces() { return namespaces; } - @SuppressWarnings("unchecked") - void setNamespaces(Collection namespaces) { + void setNamespaces(Set namespaces) { if (!namespaces.equals(this.namespaces)) { - this.namespaces = namespaces.stream().map(String::trim).collect(Collectors.toSet()); + this.namespaces = sanitizeNamespaces(namespaces); wereNamespacesSet = true; // propagate namespace changes to the dependents' config if needed - this.dependentsMetadata.forEach((name, spec) -> { + propagateNamespacesToDependents(); + } + } + + private static Set sanitizeNamespaces(Set namespaces) { + return namespaces.stream().map(String::trim).collect(Collectors.toSet()); + } + + /** + * Record potentially user-set namespaces, updating the dependent resources, which should have been set before this method + * is called. Note that this method won't affect the status of whether the namespaces were set by the user or not, as this + * should have been recorded already when the instance was created. + * This method, while public for visibility purpose from the deployment module, should be considered internal and *NOT* be + * called from user code. + */ + @SuppressWarnings("unchecked") + public void propagateNamespacesToDependents() { + if (workflow != null) { + dependentsMetadata().forEach((unused, spec) -> { final var config = spec.getDependentResourceConfig(); if (config instanceof QuarkusKubernetesDependentResourceConfig) { final var qConfig = (QuarkusKubernetesDependentResourceConfig) config; - qConfig.setNamespaces(this.namespaces); + qConfig.setNamespaces(namespaces); } }); } @@ -234,18 +247,19 @@ public boolean isStatusPresentAndNotVoid() { } public boolean areDependentsImpactedBy(Set changedClasses) { - return dependentsMetadata.keySet().parallelStream().anyMatch(changedClasses::contains); + return dependentsMetadata().keySet().parallelStream().anyMatch(changedClasses::contains); } public boolean needsDependentBeansCreation() { + final var dependentsMetadata = dependentsMetadata(); return dependentsMetadata != null && !dependentsMetadata.isEmpty(); } - public ManagedWorkflow getWorkflow() { + public QuarkusManagedWorkflow getWorkflow() { return workflow; } - public void setWorkflow(ManagedWorkflow workflow) { + public void setWorkflow(QuarkusManagedWorkflow workflow) { this.workflow = workflow; } @@ -255,8 +269,8 @@ public Object getConfigurationFor(DependentResourceSpec dependentResourceSpec) { } @Override - public List getDependentResources() { - return dependentsMetadata.values().parallelStream().collect(Collectors.toList()); + public Optional getWorkflowSpec() { + return workflow.getGenericSpec(); } @Override @@ -328,10 +342,8 @@ public Class getRateLimiterClass() { return rateLimiterClass; } - // for Quarkus' RecordableConstructor - @SuppressWarnings("unused") - public Map> getDependentsMetadata() { - return dependentsMetadata; + public Map dependentsMetadata() { + return workflow.getSpec().map(QuarkusWorkflowSpec::getDependentResourceSpecMetadata).orElse(Collections.emptyMap()); } void initAnnotationConfigurables(Reconciler reconciler) { diff --git a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusManagedWorkflow.java b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusManagedWorkflow.java index 464e4880..d003d6c2 100644 --- a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusManagedWorkflow.java +++ b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusManagedWorkflow.java @@ -1,26 +1,29 @@ package io.quarkiverse.operatorsdk.runtime; import java.util.List; +import java.util.Optional; import io.fabric8.kubernetes.api.model.HasMetadata; import io.fabric8.kubernetes.client.KubernetesClient; import io.javaoperatorsdk.operator.api.config.ControllerConfiguration; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec; import io.javaoperatorsdk.operator.processing.dependent.workflow.DefaultManagedWorkflow; -import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow; import io.javaoperatorsdk.operator.processing.dependent.workflow.Workflow; +import io.quarkus.runtime.annotations.IgnoreProperty; import io.quarkus.runtime.annotations.RecordableConstructor; @SuppressWarnings("rawtypes") public class QuarkusManagedWorkflow

extends DefaultManagedWorkflow

{ + private final QuarkusWorkflowSpec spec; public static final Workflow noOpWorkflow = new NoOpWorkflow(); - public static final ManagedWorkflow noOpManagedWorkflow = new NoOpManagedWorkflow(); + public static final QuarkusManagedWorkflow noOpManagedWorkflow = new NoOpManagedWorkflow(); public static class NoOpWorkflow

implements Workflow

{ } - public static class NoOpManagedWorkflow

implements ManagedWorkflow

{ + public static class NoOpManagedWorkflow

extends QuarkusManagedWorkflow

{ @Override @SuppressWarnings("unchecked") @@ -30,10 +33,15 @@ public Workflow

resolve(KubernetesClient kubernetesClient, } } + private QuarkusManagedWorkflow() { + this(null, List.of(), false); + } + @RecordableConstructor - public QuarkusManagedWorkflow(List orderedSpecs, + public QuarkusManagedWorkflow(QuarkusWorkflowSpec nullableSpec, List orderedSpecs, boolean hasCleaner) { super(orderedSpecs, hasCleaner); + this.spec = nullableSpec; } // Needed for the recordable constructor @@ -41,4 +49,20 @@ public QuarkusManagedWorkflow(List orderedSpecs, public boolean isHasCleaner() { return hasCleaner(); } + + // Needed for the recordable constructor + @SuppressWarnings("unused") + public QuarkusWorkflowSpec getNullableSpec() { + return spec; + } + + @IgnoreProperty + public Optional getSpec() { + return Optional.ofNullable(spec); + } + + @IgnoreProperty + public Optional getGenericSpec() { + return Optional.ofNullable(spec); + } } diff --git a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusWorkflowSpec.java b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusWorkflowSpec.java new file mode 100644 index 00000000..1a0bc491 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/QuarkusWorkflowSpec.java @@ -0,0 +1,37 @@ +package io.quarkiverse.operatorsdk.runtime; + +import java.util.List; +import java.util.Map; + +import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec; +import io.quarkus.runtime.annotations.IgnoreProperty; +import io.quarkus.runtime.annotations.RecordableConstructor; + +@SuppressWarnings("rawtypes") +public class QuarkusWorkflowSpec implements WorkflowSpec { + private final boolean explicitInvocation; + private final Map dependentResourceSpecMetadata; + + @RecordableConstructor + public QuarkusWorkflowSpec(Map dependentResourceSpecMetadata, + boolean explicitInvocation) { + this.dependentResourceSpecMetadata = dependentResourceSpecMetadata; + this.explicitInvocation = explicitInvocation; + } + + @IgnoreProperty + @Override + public List getDependentResourceSpecs() { + return dependentResourceSpecMetadata.values().stream().map(DependentResourceSpec.class::cast).toList(); + } + + public Map getDependentResourceSpecMetadata() { + return dependentResourceSpecMetadata; + } + + @Override + public boolean isExplicitInvocation() { + return explicitInvocation; + } +} diff --git a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/devconsole/ControllerInfo.java b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/devconsole/ControllerInfo.java index 9217ff0e..90c6ca3c 100644 --- a/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/devconsole/ControllerInfo.java +++ b/core/runtime/src/main/java/io/quarkiverse/operatorsdk/runtime/devconsole/ControllerInfo.java @@ -6,6 +6,7 @@ import java.util.stream.Collectors; import io.fabric8.kubernetes.api.model.HasMetadata; +import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec; import io.javaoperatorsdk.operator.processing.Controller; public class ControllerInfo

{ @@ -17,7 +18,9 @@ public class ControllerInfo

{ @SuppressWarnings({ "rawtypes", "unchecked" }) public ControllerInfo(Controller

controller) { this.controller = controller; - dependents = controller.getConfiguration().getDependentResources().stream() + dependents = controller.getConfiguration().getWorkflowSpec().stream() + .map(WorkflowSpec::getDependentResourceSpecs) + .flatMap(List::stream) .map(spec -> new DependentInfo(spec)) .sorted() .collect(Collectors.toCollection(LinkedHashSet::new)); diff --git a/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/AnnotatedDependentReconciler.java b/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/AnnotatedDependentReconciler.java index d02e97b3..b5e405ec 100644 --- a/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/AnnotatedDependentReconciler.java +++ b/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/AnnotatedDependentReconciler.java @@ -7,9 +7,11 @@ import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Workflow; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; -@ControllerConfiguration(name = NAME, dependents = @Dependent(type = AnnotatedDependentResource.class)) +@Workflow(dependents = @Dependent(type = AnnotatedDependentResource.class)) +@ControllerConfiguration(name = NAME) public class AnnotatedDependentReconciler implements Reconciler { public static final String NAME = "annotated-dependent"; diff --git a/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/DependentDefiningReconciler.java b/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/DependentDefiningReconciler.java index 8ff27887..8115c000 100644 --- a/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/DependentDefiningReconciler.java +++ b/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/DependentDefiningReconciler.java @@ -5,14 +5,16 @@ import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Workflow; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; // Note that this reconciler implementation and its dependents are not meant to be realistic but // rather exercise some of the features -@ControllerConfiguration(name = DependentDefiningReconciler.NAME, dependents = { +@Workflow(dependents = { @Dependent(type = ReadOnlyDependentResource.class, name = ReadOnlyDependentResource.NAME, readyPostcondition = ReadOnlyDependentResource.ReadOnlyReadyCondition.class), @Dependent(type = CRUDDependentResource.class, name = "crud", dependsOn = "read-only") }) +@ControllerConfiguration(name = DependentDefiningReconciler.NAME) public class DependentDefiningReconciler implements Reconciler { public static final String NAME = "dependent"; diff --git a/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/OperatorSDKResource.java b/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/OperatorSDKResource.java index 421dc4b9..a466205c 100644 --- a/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/OperatorSDKResource.java +++ b/integration-tests/src/main/java/io/quarkiverse/operatorsdk/it/OperatorSDKResource.java @@ -2,6 +2,7 @@ import java.time.Duration; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -23,6 +24,7 @@ import io.javaoperatorsdk.operator.api.config.Version; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver; import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceSpec; +import io.javaoperatorsdk.operator.api.config.workflow.WorkflowSpec; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentResourceConfig; import io.javaoperatorsdk.operator.processing.dependent.workflow.ManagedWorkflow; @@ -210,7 +212,8 @@ public String getLabelSelector() { } public List getDependents() { - final var dependents = conf.getDependentResources(); + final var dependents = conf.getWorkflowSpec().map(WorkflowSpec::getDependentResourceSpecs) + .orElse(Collections.emptyList()); final var result = new ArrayList(dependents.size()); return dependents.stream() .map(spec -> new JSONDependentResourceSpec(spec, conf)) diff --git a/integration-tests/src/test/java/io/quarkiverse/operatorsdk/it/SetOperatorLevelNamespacesTest.java b/integration-tests/src/test/java/io/quarkiverse/operatorsdk/it/SetOperatorLevelNamespacesTest.java index d53fc500..75e8987e 100644 --- a/integration-tests/src/test/java/io/quarkiverse/operatorsdk/it/SetOperatorLevelNamespacesTest.java +++ b/integration-tests/src/test/java/io/quarkiverse/operatorsdk/it/SetOperatorLevelNamespacesTest.java @@ -28,7 +28,7 @@ void configurationForControllerShouldExistAndUseOperatorLevelConfigurationWhenSe "name", equalTo(TestReconciler.NAME), "watchCurrentNamespace", is(false), "namespaces", hasSize(1), - "namespaces", hasItem("operator-level"), + "namespaces", hasItem("operator-level"), // namespace is set at the operator level by the TestProfile, so the namespace value should match what was set there "retry.maxAttempts", equalTo(1), "generationAware", equalTo(false), "maxReconciliationIntervalSeconds", equalTo(TestReconciler.INTERVAL)); diff --git a/samples/exposedapp/src/main/java/io/halkyon/DeploymentDependent.java b/samples/exposedapp/src/main/java/io/halkyon/DeploymentDependent.java index 5172d282..fce46315 100644 --- a/samples/exposedapp/src/main/java/io/halkyon/DeploymentDependent.java +++ b/samples/exposedapp/src/main/java/io/halkyon/DeploymentDependent.java @@ -23,7 +23,7 @@ public DeploymentDependent() { @SuppressWarnings("unchecked") public Deployment desired(ExposedApp exposedApp, Context context) { - final var labels = (Map) context.managedDependentResourceContext() + final var labels = (Map) context.managedWorkflowAndDependentResourceContext() .getMandatory(LABELS_CONTEXT_KEY, Map.class); final var name = exposedApp.getMetadata().getName(); final var spec = exposedApp.getSpec(); diff --git a/samples/exposedapp/src/main/java/io/halkyon/ExposedAppReconciler.java b/samples/exposedapp/src/main/java/io/halkyon/ExposedAppReconciler.java index 9d1c76c2..1d9e4c51 100644 --- a/samples/exposedapp/src/main/java/io/halkyon/ExposedAppReconciler.java +++ b/samples/exposedapp/src/main/java/io/halkyon/ExposedAppReconciler.java @@ -15,11 +15,12 @@ import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.quarkiverse.operatorsdk.annotations.CSVMetadata; -@ControllerConfiguration(namespaces = WATCH_CURRENT_NAMESPACE, name = "exposedapp", dependents = { +@Workflow(dependents = { @Dependent(type = DeploymentDependent.class), @Dependent(name = "service", type = ServiceDependent.class), @Dependent(type = IngressDependent.class, readyPostcondition = IngressDependent.class) }) +@ControllerConfiguration(namespaces = WATCH_CURRENT_NAMESPACE, name = "exposedapp") @CSVMetadata(displayName = "ExposedApp operator", description = "A sample operator that shows how to use JOSDK's main features with the Quarkus extension") public class ExposedAppReconciler implements Reconciler, ContextInitializer { @@ -34,14 +35,14 @@ public ExposedAppReconciler() { @Override public void initContext(ExposedApp exposedApp, Context context) { final var labels = Map.of(APP_LABEL, exposedApp.getMetadata().getName()); - context.managedDependentResourceContext().put(LABELS_CONTEXT_KEY, labels); + context.managedWorkflowAndDependentResourceContext().put(LABELS_CONTEXT_KEY, labels); } @Override public UpdateControl reconcile(ExposedApp exposedApp, Context context) { final var name = exposedApp.getMetadata().getName(); // retrieve the workflow reconciliation result and re-schedule if we have dependents that are not yet ready - final var wrs = context.managedDependentResourceContext().getWorkflowReconcileResult(); + final var wrs = context.managedWorkflowAndDependentResourceContext().getWorkflowReconcileResult(); if (wrs.allDependentResourcesReady()) { final var url = IngressDependent.getExposedURL( diff --git a/samples/exposedapp/src/main/java/io/halkyon/IngressDependent.java b/samples/exposedapp/src/main/java/io/halkyon/IngressDependent.java index 7462b672..96e95cca 100644 --- a/samples/exposedapp/src/main/java/io/halkyon/IngressDependent.java +++ b/samples/exposedapp/src/main/java/io/halkyon/IngressDependent.java @@ -22,7 +22,7 @@ public IngressDependent() { @Override @SuppressWarnings("unchecked") public Ingress desired(ExposedApp exposedApp, Context context) { - final var labels = (Map) context.managedDependentResourceContext() + final var labels = (Map) context.managedWorkflowAndDependentResourceContext() .getMandatory(LABELS_CONTEXT_KEY, Map.class); final var metadata = createMetadata(exposedApp, labels); /* diff --git a/samples/exposedapp/src/main/java/io/halkyon/ServiceDependent.java b/samples/exposedapp/src/main/java/io/halkyon/ServiceDependent.java index 4445d6f2..338aa26b 100644 --- a/samples/exposedapp/src/main/java/io/halkyon/ServiceDependent.java +++ b/samples/exposedapp/src/main/java/io/halkyon/ServiceDependent.java @@ -19,7 +19,7 @@ public ServiceDependent() { @Override @SuppressWarnings("unchecked") public Service desired(ExposedApp exposedApp, Context context) { - final var labels = (Map) context.managedDependentResourceContext() + final var labels = (Map) context.managedWorkflowAndDependentResourceContext() .getMandatory(LABELS_CONTEXT_KEY, Map.class); return new ServiceBuilder() diff --git a/samples/mysql-schema/src/main/java/io/quarkiverse/operatorsdk/samples/mysqlschema/MySQLSchemaReconciler.java b/samples/mysql-schema/src/main/java/io/quarkiverse/operatorsdk/samples/mysqlschema/MySQLSchemaReconciler.java index 6fd59038..4fd70b3d 100644 --- a/samples/mysql-schema/src/main/java/io/quarkiverse/operatorsdk/samples/mysqlschema/MySQLSchemaReconciler.java +++ b/samples/mysql-schema/src/main/java/io/quarkiverse/operatorsdk/samples/mysqlschema/MySQLSchemaReconciler.java @@ -12,6 +12,7 @@ import io.javaoperatorsdk.operator.api.reconciler.ErrorStatusUpdateControl; import io.javaoperatorsdk.operator.api.reconciler.Reconciler; import io.javaoperatorsdk.operator.api.reconciler.UpdateControl; +import io.javaoperatorsdk.operator.api.reconciler.Workflow; import io.javaoperatorsdk.operator.api.reconciler.dependent.Dependent; import io.quarkiverse.operatorsdk.samples.mysqlschema.dependent.SchemaDependentResource; import io.quarkiverse.operatorsdk.samples.mysqlschema.dependent.SecretDependentResource; @@ -19,10 +20,12 @@ import io.quarkiverse.operatorsdk.samples.mysqlschema.schema.SchemaService; import io.quarkus.logging.Log; -@ControllerConfiguration(dependents = { +@Workflow(dependents = { @Dependent(type = SecretDependentResource.class), @Dependent(type = SchemaDependentResource.class, name = SchemaDependentResource.NAME) }) +@ControllerConfiguration +@SuppressWarnings("unused") public class MySQLSchemaReconciler implements Reconciler, ErrorStatusHandler { @@ -33,14 +36,15 @@ public class MySQLSchemaReconciler public UpdateControl reconcile(MySQLSchema schema, Context context) { // we only need to update the status if we just built the schema, i.e. when it's // present in the context - return context.getSecondaryResource(Secret.class).map(secret -> { - return context.getSecondaryResource(Schema.class, SchemaDependentResource.NAME).map(s -> { - updateStatusPojo(schema, secret.getMetadata().getName(), - decode(secret.getData().get(MYSQL_SECRET_USERNAME))); - Log.infof("Schema %s created - updating CR status", s.getName()); - return UpdateControl.updateStatus(schema); - }).orElse(UpdateControl.noUpdate()); - }).orElse(UpdateControl.noUpdate()); + return context.getSecondaryResource(Secret.class) + .map(secret -> context.getSecondaryResource(Schema.class, SchemaDependentResource.NAME) + .map(s -> { + updateStatusPojo(schema, secret.getMetadata().getName(), + decode(secret.getData().get(MYSQL_SECRET_USERNAME))); + Log.infof("Schema %s created - updating CR status", s.getName()); + return UpdateControl.updateStatus(schema); + }).orElse(UpdateControl.noUpdate())) + .orElse(UpdateControl.noUpdate()); } @Override