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

io.quarkiverse.operatorsdk.deployment.QuarkusControllerConfigurationBuilder 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.loadClass;
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.KUBERNETES_DEPENDENT;
import static io.quarkiverse.operatorsdk.common.Constants.KUBERNETES_DEPENDENT_RESOURCE;
import static io.quarkus.arc.processor.DotNames.APPLICATION_SCOPED;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.IndexView;
import org.jboss.logging.Logger;

import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.client.CustomResource;
import io.javaoperatorsdk.operator.ReconcilerUtils;
import io.javaoperatorsdk.operator.api.reconciler.dependent.DependentResource;
import io.quarkiverse.operatorsdk.common.ClassUtils.ReconcilerInfo;
import io.quarkiverse.operatorsdk.common.ConfigurationUtils;
import io.quarkiverse.operatorsdk.runtime.BuildTimeOperatorConfiguration;
import io.quarkiverse.operatorsdk.runtime.QuarkusControllerConfiguration;
import io.quarkiverse.operatorsdk.runtime.QuarkusDependentResourceSpec;
import io.quarkiverse.operatorsdk.runtime.QuarkusKubernetesDependentResourceConfig;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.builder.BuildException;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.builditem.LiveReloadBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ForceNonWeakReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.util.JandexUtil;

@SuppressWarnings("rawtypes")
class QuarkusControllerConfigurationBuilder {
    static final Logger log = OperatorSDKProcessor.log;

    private final BuildProducer additionalBeans;
    private final BuildProducer reflectionClasses;
    private final BuildProducer forcedReflectionClasses;
    private final IndexView index;
    private final CRDGeneration crdGeneration;
    private final LiveReloadBuildItem liveReload;

    private final BuildTimeOperatorConfiguration buildTimeConfiguration;

    public QuarkusControllerConfigurationBuilder(BuildProducer additionalBeans,
            BuildProducer reflectionClasses,
            BuildProducer forcedReflectionClasses, IndexView index,
            CRDGeneration crdGeneration, LiveReloadBuildItem liveReload,
            BuildTimeOperatorConfiguration buildTimeConfiguration) {
        this.additionalBeans = additionalBeans;
        this.reflectionClasses = reflectionClasses;
        this.forcedReflectionClasses = forcedReflectionClasses;
        this.index = index;
        this.crdGeneration = crdGeneration;
        this.liveReload = liveReload;
        this.buildTimeConfiguration = buildTimeConfiguration;
    }

    @SuppressWarnings("unchecked")
    QuarkusControllerConfiguration build(ReconcilerInfo reconcilerInfo) {
        final var primaryTypeDN = reconcilerInfo.primaryTypeName();
        final var primaryTypeName = primaryTypeDN.toString();

        // retrieve the reconciler's name
        final var info = reconcilerInfo.classInfo();
        final var reconcilerClassName = info.toString();
        final String name = reconcilerInfo.name();

        // create Reconciler bean
        additionalBeans.produce(
                AdditionalBeanBuildItem.builder()
                        .addBeanClass(reconcilerClassName)
                        .setUnremovable()
                        .setDefaultScope(APPLICATION_SCOPED)
                        .build());

        // register target resource class for introspection
        registerForReflection(reflectionClasses, primaryTypeName);
        forcedReflectionClasses.produce(new ForceNonWeakReflectiveClassBuildItem(primaryTypeName));

        // register spec and status for introspection if we're targeting a CustomResource
        final var primaryCI = index.getClassByName(primaryTypeDN);
        boolean isCR = false;
        if (primaryCI == null) {
            log.warnv(
                    "''{0}'' has not been found in the Jandex index so it cannot be introspected. Assumed not to be a CustomResource implementation. If you believe this is wrong, please index your classes with Jandex.",
                    primaryTypeDN);
        } else {
            try {
                isCR = JandexUtil.isSubclassOf(index, primaryCI, CUSTOM_RESOURCE);
            } catch (BuildException e) {
                log.errorv("Couldn't ascertain if ''{0}'' is a CustomResource subclass. Assumed not to be.",
                        e);
            }
        }

        String specClassName = null;
        String statusClassName = null;
        if (isCR) {
            final var crParamTypes = JandexUtil.resolveTypeParameters(primaryTypeDN, CUSTOM_RESOURCE,
                    index);
            specClassName = crParamTypes.get(0).name().toString();
            statusClassName = crParamTypes.get(1).name().toString();
            registerForReflection(reflectionClasses, specClassName);
            registerForReflection(reflectionClasses, statusClassName);
        }

        // now check if there's more work to do, depending on reloaded state
        Class resourceClass = null;
        String resourceFullName = null;

        // check if we need to regenerate the CRD
        final var changeInformation = liveReload.getChangeInformation();
        if (isCR && crdGeneration.wantCRDGenerated()) {
            // check whether we already have generated CRDs
            var storedCRDInfos = liveReload.getContextObject(ContextStoredCRDInfos.class);

            final boolean[] generateCurrent = { true }; // request CRD generation by default

            final var crClass = loadClass(primaryTypeName, CustomResource.class);
            resourceClass = crClass;
            resourceFullName = getFullResourceName(crClass);

            // When we have a live reload, check if we need to regenerate the associated CRD
            if (liveReload.isLiveReload() && storedCRDInfos != null) {
                final var finalCrdName = resourceFullName;
                final var crdInfos = storedCRDInfos.getCRDInfosFor(resourceFullName);

                // check for all CRD spec version requested
                buildTimeConfiguration.crd.versions.forEach(v -> {
                    final var crd = crdInfos.get(v);
                    // if we don't have any information about this CRD version, we need to generate the CRD
                    if (crd == null) {
                        return;
                    }

                    // if dependent classes have been changed
                    if (changeInformation != null) {
                        for (String changedClass : changeInformation.getChangedClasses()) {
                            if (crd.getDependentClassNames().contains(changedClass)) {
                                return; // a dependent class has been changed, so we'll need to generate the CRD
                            }
                        }
                    }

                    // we've looked at all the changed classes and none have been changed for this CR/version: do not regenerate CRD
                    generateCurrent[0] = false;
                    log.infov(
                            "''{0}'' CRD generation was skipped for ''{1}'' because no changes impacting the CRD were detected",
                            v, finalCrdName);
                });
            }
            // if we still need to generate the CRD, add the CR to the set to be generated
            if (generateCurrent[0]) {
                crdGeneration.withCustomResource(crClass, resourceFullName, name);
            }
        }

        // check if we need to regenerate the configuration for this controller
        QuarkusControllerConfiguration configuration = null;
        var storedConfigurations = liveReload.getContextObject(
                ContextStoredControllerConfigurations.class);
        if (liveReload.isLiveReload() && storedConfigurations != null) {
            // check if we need to regenerate the configuration for this controller
            final var changedClasses = changeInformation == null ? Collections. emptySet()
                    : changeInformation.getChangedClasses();
            final var changedResources = liveReload.getChangedResources();
            configuration = storedConfigurations.configurationOrNullIfNeedGeneration(reconcilerClassName,
                    changedClasses,
                    changedResources);

        }

        if (configuration == null) {
            // extract the configuration from annotation and/or external configuration
            final var controllerAnnotation = info.classAnnotation(CONTROLLER_CONFIGURATION);

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

            if (resourceFullName == null) {
                resourceClass = loadClass(primaryTypeName, HasMetadata.class);
                resourceFullName = getFullResourceName(resourceClass);
            }

            final var crVersion = HasMetadata.getVersion(resourceClass);

            // extract the namespaces
            final var namespaces = configExtractor.namespaces(name);

            final var dependentResources = createDependentResources(
                    reflectionClasses, index, controllerAnnotation, namespaces, additionalBeans);

            // create the configuration
            configuration = new QuarkusControllerConfiguration(
                    reconcilerClassName,
                    name,
                    resourceFullName,
                    crVersion,
                    configExtractor.generationAware(),
                    resourceClass,
                    namespaces,
                    getFinalizer(controllerAnnotation, resourceFullName),
                    getLabelSelector(controllerAnnotation),
                    Optional.ofNullable(specClassName),
                    Optional.ofNullable(statusClassName),
                    dependentResources.values().stream().collect(Collectors.toUnmodifiableList()));

            log.infov(
                    "Processed ''{0}'' reconciler named ''{1}'' for ''{2}'' resource (version ''{3}'')",
                    reconcilerClassName, name, resourceFullName, HasMetadata.getApiVersion(resourceClass));
        } else {
            log.infov("Skipped configuration reload for ''{0}'' reconciler as no changes were detected",
                    reconcilerClassName);
        }

        // store the configuration in the live reload context
        if (storedConfigurations == null) {
            storedConfigurations = new ContextStoredControllerConfigurations();
        }
        storedConfigurations.recordConfiguration(configuration);
        liveReload.setContextObject(ContextStoredControllerConfigurations.class, storedConfigurations);

        return configuration;
    }

    @SuppressWarnings("unchecked")
    private Map createDependentResources(
            BuildProducer reflectionClasses, IndexView index,
            AnnotationInstance controllerAnnotation, Set namespaces,
            BuildProducer additionalBeans) {
        // deal with dependent resources
        var dependentResources = Collections. emptyMap();
        if (controllerAnnotation != null) {
            final var dependents = controllerAnnotation.value("dependents");
            if (dependents != null) {
                final var dependentAnnotations = dependents.asNestedArray();
                dependentResources = new LinkedHashMap<>(dependentAnnotations.length);
                for (AnnotationInstance dependentConfig : dependentAnnotations) {
                    final var dependentTypeDN = dependentConfig.value("type").asClass().name();
                    final var dependentType = index.getClassByName(dependentTypeDN);
                    if (!dependentType.hasNoArgsConstructor()) {
                        throw new IllegalArgumentException(
                                "DependentResource implementations must provide a no-arg constructor for instantiation purposes");
                    }

                    final var dependentTypeName = dependentTypeDN.toString();
                    final var dependentClass = loadClass(dependentTypeName, DependentResource.class);
                    registerForReflection(reflectionClasses, dependentTypeName);

                    // further process Kubernetes dependents
                    final boolean isKubernetesDependent;
                    try {
                        isKubernetesDependent = JandexUtil.isSubclassOf(index, dependentType,
                                KUBERNETES_DEPENDENT_RESOURCE);
                    } catch (BuildException e) {
                        throw new IllegalStateException(
                                "DependentResource " + dependentType + " is not indexed", e);
                    }
                    Object cfg = null;
                    if (isKubernetesDependent) {
                        final var kubeDepConfig = dependentType.classAnnotation(KUBERNETES_DEPENDENT);
                        final var labelSelector = getLabelSelector(kubeDepConfig);
                        // if the dependent doesn't explicitly provide a namespace configuration, inherit the configuration from the reconciler configuration
                        final Set dependentNamespaces = ConfigurationUtils.annotationValueOrDefault(
                                kubeDepConfig,
                                "namespaces", v -> new HashSet<>(
                                        Arrays.asList(v.asStringArray())),
                                () -> namespaces);
                        cfg = new QuarkusKubernetesDependentResourceConfig(dependentNamespaces, labelSelector);
                    }

                    var nameField = dependentConfig.value("name");
                    final var name = Optional.ofNullable(nameField)
                            .map(AnnotationValue::asString)
                            .filter(Predicate.not(String::isBlank))
                            .orElse(DependentResource.defaultNameFor(dependentClass));
                    final var spec = dependentResources.get(name);
                    if (spec != null) {
                        throw new IllegalArgumentException(
                                "A DependentResource named: " + name + " already exists: " + spec);
                    }
                    dependentResources.put(name, new QuarkusDependentResourceSpec(dependentClass, cfg, name));

                    additionalBeans.produce(
                            AdditionalBeanBuildItem.builder()
                                    .addBeanClass(dependentTypeName)
                                    .setUnremovable()
                                    .setDefaultScope(APPLICATION_SCOPED)
                                    .build());
                }
            }
        }
        return dependentResources;
    }

    private String getFullResourceName(Class crClass) {
        return ReconcilerUtils.getResourceTypeName(crClass);
    }

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

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

    private void registerForReflection(
            BuildProducer reflectionClasses, String className) {
        Optional.ofNullable(className)
                .filter(s -> !className.startsWith("java."))
                .ifPresent(
                        cn -> {
                            reflectionClasses.produce(new ReflectiveClassBuildItem(true, true, cn));
                            log.infov("Registered ''{0}'' for reflection", cn);
                        });
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy