All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.quarkiverse.operatorsdk.deployment.QuarkusControllerConfigurationBuildStep Maven / Gradle / Ivy

There is a newer version: 6.8.4
Show newest version
package io.quarkiverse.operatorsdk.deployment;

import static io.quarkiverse.operatorsdk.common.ClassLoadingUtils.instantiate;
import static io.quarkiverse.operatorsdk.common.ClassLoadingUtils.loadClass;
import static io.quarkiverse.operatorsdk.common.Constants.*;

import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.jboss.jandex.*;
import org.jboss.logging.Logger;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.rbac.PolicyRule;
import io.fabric8.kubernetes.api.model.rbac.PolicyRuleBuilder;
import io.fabric8.kubernetes.api.model.rbac.RoleRef;
import io.fabric8.kubernetes.api.model.rbac.RoleRefBuilder;
import io.fabric8.kubernetes.client.informers.cache.ItemStore;
import io.javaoperatorsdk.operator.ReconcilerUtils;
import io.javaoperatorsdk.operator.api.config.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.config.dependent.DependentResourceConfigurationResolver;
import io.javaoperatorsdk.operator.api.reconciler.MaxReconciliationInterval;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependent;
import io.javaoperatorsdk.operator.processing.dependent.kubernetes.KubernetesDependentConverter;
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.event.rate.RateLimiter;
import io.javaoperatorsdk.operator.processing.event.source.controller.ResourceEventFilter;
import io.javaoperatorsdk.operator.processing.event.source.filter.GenericFilter;
import io.javaoperatorsdk.operator.processing.event.source.filter.OnAddFilter;
import io.javaoperatorsdk.operator.processing.event.source.filter.OnUpdateFilter;
import io.javaoperatorsdk.operator.processing.retry.GenericRetry;
import io.javaoperatorsdk.operator.processing.retry.Retry;
import io.quarkiverse.operatorsdk.annotations.RBACCRoleRef;
import io.quarkiverse.operatorsdk.common.AnnotationConfigurableAugmentedClassInfo;
import io.quarkiverse.operatorsdk.common.ClassLoadingUtils;
import io.quarkiverse.operatorsdk.common.ConfigurationUtils;
import io.quarkiverse.operatorsdk.common.Constants;
import io.quarkiverse.operatorsdk.common.DependentResourceAugmentedClassInfo;
import io.quarkiverse.operatorsdk.common.ReconciledAugmentedClassInfo;
import io.quarkiverse.operatorsdk.common.ReconcilerAugmentedClassInfo;
import io.quarkiverse.operatorsdk.common.SelectiveAugmentedClassInfo;
import io.quarkiverse.operatorsdk.runtime.*;
import io.quarkiverse.operatorsdk.runtime.QuarkusControllerConfiguration.DefaultRateLimiter;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.LiveReloadBuildItem;
import io.quarkus.deployment.util.JandexUtil;

@SuppressWarnings("rawtypes")
class QuarkusControllerConfigurationBuildStep {

    static final Logger log = Logger.getLogger(QuarkusControllerConfigurationBuildStep.class.getName());

    private static final KubernetesDependentConverter KUBERNETES_DEPENDENT_CONVERTER = new KubernetesDependentConverter() {
        @Override
        @SuppressWarnings("unchecked")
        public KubernetesDependentResourceConfig configFrom(
                KubernetesDependent configAnnotation,
                ControllerConfiguration parentConfiguration, Class originatingClass) {
            final var original = super.configFrom(configAnnotation, parentConfiguration, originatingClass);
            // make the configuration bytecode-serializable
            return new QuarkusKubernetesDependentResourceConfig(original.namespaces(),
                    original.labelSelector(),
                    original.wereNamespacesConfigured(),
                    original.createResourceOnlyIfNotExistingWithSSA(), original.getResourceDiscriminator(),
                    (Boolean) original.useSSA().orElse(null),
                    original.onAddFilter(),
                    original.onUpdateFilter(), original.onDeleteFilter(), original.genericFilter());
        }
    };
    static {
        // register Quarkus-specific converter for Kubernetes dependent resources
        DependentResourceConfigurationResolver.registerConverter(KubernetesDependentResource.class,
                KUBERNETES_DEPENDENT_CONVERTER);
    }

    @BuildStep
    @SuppressWarnings("unused")
    ControllerConfigurationsBuildItem createControllerConfigurations(
            BuildTimeConfigurationServiceBuildItem buildTimeConfigurationServiceBuildItem,
            ReconcilerInfosBuildItem reconcilers,
            AnnotationConfigurablesBuildItem annotationConfigurables,
            BuildTimeOperatorConfiguration buildTimeConfiguration,
            CombinedIndexBuildItem combinedIndexBuildItem,
            LiveReloadBuildItem liveReload) {

        final var maybeStoredConfigurations = liveReload.getContextObject(ContextStoredControllerConfigurations.class);
        var storedConfigurations = maybeStoredConfigurations != null ? maybeStoredConfigurations
                : new ContextStoredControllerConfigurations();

        liveReload.setContextObject(ContextStoredControllerConfigurations.class, storedConfigurations);
        final var changedClasses = ConfigurationUtils.getChangedClasses(liveReload);
        final var changedResources = liveReload.getChangedResources();

        final List collect = reconcilers.getReconcilers()
                .values()
                .stream()
                .map(reconcilerInfo -> {
                    // retrieve the reconciler's name
                    final var info = reconcilerInfo.classInfo();
                    final var reconcilerClassName = info.toString();

                    QuarkusControllerConfiguration configuration = null;
                    if (liveReload.isLiveReload()) {
                        // check if we need to regenerate the configuration for this controller
                        configuration = storedConfigurations.configurationOrNullIfNeedGeneration(
                                reconcilerClassName,
                                changedClasses,
                                changedResources);
                    }

                    if (configuration == null) {
                        configuration = createConfiguration(reconcilerInfo,
                                annotationConfigurables.getConfigurableInfos(),
                                buildTimeConfigurationServiceBuildItem.getConfigurationService(),
                                buildTimeConfiguration,
                                combinedIndexBuildItem.getIndex());
                    }

                    // store the configuration in the live reload context
                    storedConfigurations.recordConfiguration(configuration);

                    return configuration;
                }).collect(Collectors.toList());

        // store configurations in the live reload context
        liveReload.setContextObject(ContextStoredControllerConfigurations.class, storedConfigurations);

        return new ControllerConfigurationsBuildItem(collect);
    }

    @SuppressWarnings("unchecked")
    static QuarkusControllerConfiguration createConfiguration(
            ReconcilerAugmentedClassInfo reconcilerInfo,
            Map configurableInfos,
            BuildTimeConfigurationService buildTimeConfigurationService,
            BuildTimeOperatorConfiguration buildTimeConfiguration,
            IndexView index) {

        final var info = reconcilerInfo.classInfo();
        final var reconcilerClassName = info.toString();
        final String name = reconcilerInfo.nameOrFailIfUnset();
        QuarkusControllerConfiguration configuration;
        // extract the configuration from annotation and/or external configuration
        final var controllerAnnotation = info.declaredAnnotation(CONTROLLER_CONFIGURATION);

        final var externalConfiguration = buildTimeConfiguration.controllers().get(name);
        final var configExtractor = new BuildTimeHybridControllerConfiguration(buildTimeConfiguration,
                externalConfiguration,
                controllerAnnotation);

        // deal with event filters
        ResourceEventFilter finalFilter = null;
        final var eventFilterTypes = ConfigurationUtils.annotationValueOrDefault(
                controllerAnnotation, "eventFilters",
                AnnotationValue::asClassArray, () -> new Type[0]);
        for (Type filterType : eventFilterTypes) {
            final var filterClass = loadClass(filterType.name().toString(), ResourceEventFilter.class);
            final var filter = instantiate(filterClass);
            finalFilter = finalFilter == null ? filter : finalFilter.and(filter);
        }

        Duration maxReconciliationInterval = null;
        OnAddFilter onAddFilter = null;
        OnUpdateFilter onUpdateFilter = null;
        GenericFilter genericFilter = null;
        Class retryClass = GenericRetry.class;
        Class retryConfigurationClass = null;
        Class rateLimiterClass = DefaultRateLimiter.class;
        Class rateLimiterConfigurationClass = null;
        Long nullableInformerListLimit = null;
        String fieldManager = null;
        ItemStore itemStore = null;
        if (controllerAnnotation != null) {
            final var intervalFromAnnotation = ConfigurationUtils.annotationValueOrDefault(
                    controllerAnnotation, "maxReconciliationInterval", AnnotationValue::asNested,
                    () -> null);
            final var interval = ConfigurationUtils.annotationValueOrDefault(
                    intervalFromAnnotation, "interval", AnnotationValue::asLong,
                    () -> MaxReconciliationInterval.DEFAULT_INTERVAL);
            final var timeUnit = ConfigurationUtils.annotationValueOrDefault(
                    intervalFromAnnotation,
                    "timeUnit",
                    av -> TimeUnit.valueOf(av.asEnum()),
                    () -> TimeUnit.HOURS);
            if (interval > 0) {
                maxReconciliationInterval = Duration.of(interval, timeUnit.toChronoUnit());
            }

            onAddFilter = ConfigurationUtils.instantiateImplementationClass(
                    controllerAnnotation, "onAddFilter", OnAddFilter.class, OnAddFilter.class, true, index);
            onUpdateFilter = ConfigurationUtils.instantiateImplementationClass(
                    controllerAnnotation, "onUpdateFilter", OnUpdateFilter.class, OnUpdateFilter.class,
                    true, index);
            genericFilter = ConfigurationUtils.instantiateImplementationClass(
                    controllerAnnotation, "genericFilter", GenericFilter.class, GenericFilter.class,
                    true, index);
            retryClass = ConfigurationUtils.annotationValueOrDefault(controllerAnnotation,
                    "retry", av -> loadClass(av.asClass().name().toString(), Retry.class), () -> GenericRetry.class);
            final var retryConfigurableInfo = configurableInfos.get(retryClass.getName());
            retryConfigurationClass = getConfigurationAnnotationClass(reconcilerInfo, retryConfigurableInfo);
            rateLimiterClass = ConfigurationUtils.annotationValueOrDefault(
                    controllerAnnotation,
                    "rateLimiter", av -> loadClass(av.asClass().name().toString(), RateLimiter.class),
                    () -> DefaultRateLimiter.class);
            final var rateLimiterConfigurableInfo = configurableInfos.get(rateLimiterClass.getName());
            rateLimiterConfigurationClass = getConfigurationAnnotationClass(reconcilerInfo,
                    rateLimiterConfigurableInfo);
            nullableInformerListLimit = ConfigurationUtils.annotationValueOrDefault(
                    controllerAnnotation, "informerListLimit", AnnotationValue::asLong,
                    () -> null);
            fieldManager = ConfigurationUtils.annotationValueOrDefault(controllerAnnotation, "fieldManager",
                    AnnotationValue::asString, () -> null);
            itemStore = ConfigurationUtils.instantiateImplementationClass(controllerAnnotation, "itemStore", ItemStore.class,
                    ItemStore.class, true, index);
        }

        // check if we have additional RBAC rules to handle
        final var additionalRBACRules = extractAdditionalRBACRules(info);

        // check if we have additional RBAC role refs to handle
        final var additionalRBACRoleRefs = extractAdditionalRBACRoleRefs(info);

        // extract the namespaces
        // first check if we explicitly set the namespaces via the annotations
        Set namespaces = null;
        if (controllerAnnotation != null) {
            namespaces = Optional.ofNullable(controllerAnnotation.value("namespaces"))
                    .map(v -> new HashSet<>(Arrays.asList(v.asStringArray())))
                    .orElse(null);
        }
        // remember whether or not we explicitly set the namespaces
        final boolean wereNamespacesSet;
        if (namespaces == null) {
            namespaces = io.javaoperatorsdk.operator.api.reconciler.Constants.DEFAULT_NAMESPACES_SET;
            wereNamespacesSet = false;
        } else {
            wereNamespacesSet = true;
        }

        // check if we're asking to generate manifests with a specific set of namespaces
        // note that this should *NOT* be considered as explicitly setting the namespaces for the purpose of runtime overriding
        final var buildTimeNamespaces = configExtractor.generateWithWatchedNamespaces(wereNamespacesSet);
        if (buildTimeNamespaces != null) {
            namespaces = buildTimeNamespaces;
        }

        // create the configuration
        final ReconciledAugmentedClassInfo primaryInfo = reconcilerInfo.associatedResourceInfo();
        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,
                resourceFullName,
                primaryAsResource.version(),
                configExtractor.generationAware(),
                resourceClass,
                nullableInformerListLimit,
                namespaces,
                wereNamespacesSet,
                getFinalizer(controllerAnnotation, resourceFullName),
                getLabelSelector(controllerAnnotation),
                primaryAsResource.hasNonVoidStatus(),
                finalFilter,
                maxReconciliationInterval,
                onAddFilter, onUpdateFilter, genericFilter, retryClass, retryConfigurationClass, rateLimiterClass,
                rateLimiterConfigurationClass, dependentResources, null, additionalRBACRules, additionalRBACRoleRefs,
                fieldManager, itemStore);

        // setting the configuration service with build-time known information for resolution of some information such as resource classes associated with dependent resources or whether SSA should be used for a given dependent
        configuration.setParent(buildTimeConfigurationService);

        if (hasDependents) {
            dependentResourceInfos.forEach(dependent -> {
                final var spec = createDependentResourceSpec(dependent, index, configuration);
                final var dependentName = dependent.classInfo().name();
                dependentResources.put(dependentName.toString(), spec);
            });
        }

        // 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);

        log.infov(
                "Processed ''{0}'' reconciler named ''{1}'' for ''{2}'' resource (version ''{3}'')",
                reconcilerClassName, name, resourceFullName, HasMetadata.getApiVersion(resourceClass));
        return configuration;
    }

    private static List extractAdditionalRBACRules(ClassInfo info) {
        return extractRepeatableAnnotations(info, RBAC_RULE, ADDITIONAL_RBAC_RULES,
                QuarkusControllerConfigurationBuildStep::extractRule);
    }

    private static  List extractRepeatableAnnotations(ClassInfo info, DotName singleAnnotationName,
            DotName repeatableHolderName, Function extractor) {
        // if there are multiple annotations they should be found under an automatically generated repeatable holder annotation
        final var additionalAnnotations = ConfigurationUtils.annotationValueOrDefault(
                info.declaredAnnotation(repeatableHolderName),
                "value",
                AnnotationValue::asNestedArray,
                () -> null);
        List repeatables = Collections.emptyList();
        if (additionalAnnotations != null && additionalAnnotations.length > 0) {
            repeatables = new ArrayList<>(additionalAnnotations.length);
            for (AnnotationInstance annotation : additionalAnnotations) {
                repeatables.add(extractor.apply(annotation));
            }
        }

        // if there's only one, it will be found under RBACRule annotation
        final var singleAnnotation = info.declaredAnnotation(singleAnnotationName);
        if (singleAnnotation != null) {
            repeatables = List.of(extractor.apply(singleAnnotation));
        }

        return repeatables;
    }

    private static List extractAdditionalRBACRoleRefs(ClassInfo info) {
        return extractRepeatableAnnotations(info, RBAC_ROLE_REF, ADDITIONAL_RBAC_ROLE_REFS,
                QuarkusControllerConfigurationBuildStep::extractRoleRef);
    }

    private static PolicyRule extractRule(AnnotationInstance ruleAnnotation) {
        final var builder = new PolicyRuleBuilder();

        builder.withApiGroups(ConfigurationUtils.annotationValueOrDefault(ruleAnnotation,
                "apiGroups",
                AnnotationValue::asStringArray,
                () -> null));

        builder.withVerbs(ConfigurationUtils.annotationValueOrDefault(ruleAnnotation,
                "verbs",
                AnnotationValue::asStringArray,
                () -> null));

        builder.withResources(ConfigurationUtils.annotationValueOrDefault(ruleAnnotation,
                "resources",
                AnnotationValue::asStringArray,
                () -> null));

        builder.withResourceNames(ConfigurationUtils.annotationValueOrDefault(ruleAnnotation,
                "resourceNames",
                AnnotationValue::asStringArray,
                () -> null));

        builder.withNonResourceURLs(ConfigurationUtils.annotationValueOrDefault(ruleAnnotation,
                "nonResourceURLs",
                AnnotationValue::asStringArray,
                () -> null));

        return builder.build();
    }

    private static RoleRef extractRoleRef(AnnotationInstance roleRefAnnotation) {
        final var builder = new RoleRefBuilder();

        builder.withApiGroup(RBACCRoleRef.RBAC_API_GROUP);
        builder.withKind(ConfigurationUtils.annotationValueOrDefault(roleRefAnnotation,
                "kind",
                AnnotationValue::asEnum,
                () -> null));

        builder.withName(ConfigurationUtils.annotationValueOrDefault(roleRefAnnotation,
                "name",
                AnnotationValue::asString,
                () -> null));

        return builder.build();
    }

    private static Class getConfigurationAnnotationClass(SelectiveAugmentedClassInfo configurationTargetInfo,
            AnnotationConfigurableAugmentedClassInfo configurableInfo) {
        if (configurableInfo != null) {
            final var associatedConfigurationClass = configurableInfo.getAssociatedConfigurationClass();
            if (configurationTargetInfo.classInfo().annotationsMap().containsKey(associatedConfigurationClass)) {
                return ClassLoadingUtils
                        .loadClass(associatedConfigurationClass.toString(), Object.class);
            }
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    private static DependentResourceSpecMetadata createDependentResourceSpec(
            DependentResourceAugmentedClassInfo dependent,
            IndexView index,
            QuarkusControllerConfiguration configuration) {
        final var dependentResourceType = dependent.classInfo();

        // resolve the associated resource type
        final var drTypeName = dependentResourceType.name();
        final var types = JandexUtil.resolveTypeParameters(drTypeName, Constants.DEPENDENT_RESOURCE,
                index);
        final String resourceTypeName;
        if (types.size() == 2) {
            resourceTypeName = types.get(0).name().toString();
        } else {
            throw new IllegalArgumentException(
                    "Improperly parameterized DependentResource implementation: " + drTypeName.toString());
        }

        final var dependentTypeName = drTypeName.toString();
        final var dependentClass = loadClass(dependentTypeName, DependentResource.class);

        final var cfg = DependentResourceConfigurationResolver.extractConfigurationFromConfigured(
                dependentClass, configuration);

        final var dependentConfig = dependent.getDependentAnnotationFromController();
        final var dependsOnField = dependentConfig.value("dependsOn");
        final var dependsOn = Optional.ofNullable(dependsOnField)
                .map(AnnotationValue::asStringArray)
                .filter(array -> array.length > 0)
                .map(Set::of).orElse(Collections.emptySet());

        final var readyCondition = ConfigurationUtils.instantiateImplementationClass(
                dependentConfig, "readyPostcondition", Condition.class,
                Condition.class, true, index);
        final var reconcilePrecondition = ConfigurationUtils.instantiateImplementationClass(
                dependentConfig, "reconcilePrecondition", Condition.class,
                Condition.class, true, index);
        final var deletePostcondition = ConfigurationUtils.instantiateImplementationClass(
                dependentConfig, "deletePostcondition", Condition.class,
                Condition.class, true, index);
        final var activationCondition = ConfigurationUtils.instantiateImplementationClass(
                dependentConfig, "activationCondition", Condition.class,
                Condition.class, true, index);

        final var useEventSourceWithName = ConfigurationUtils.annotationValueOrDefault(
                dependentConfig, "useEventSourceWithName", AnnotationValue::asString,
                () -> null);

        return new DependentResourceSpecMetadata(dependentClass, cfg, dependent.nameOrFailIfUnset(),
                dependsOn, readyCondition, reconcilePrecondition, deletePostcondition, activationCondition,
                useEventSourceWithName,
                resourceTypeName);

    }

    private static String getFinalizer(AnnotationInstance controllerAnnotation, String crdName) {
        return ConfigurationUtils.annotationValueOrDefault(controllerAnnotation,
                "finalizerName",
                AnnotationValue::asString,
                () -> ReconcilerUtils.getDefaultFinalizerName(crdName));
    }

    private static String getLabelSelector(AnnotationInstance controllerAnnotation) {
        return ConfigurationUtils.annotationValueOrDefault(controllerAnnotation,
                "labelSelector",
                AnnotationValue::asString,
                () -> null);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy