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

io.quarkiverse.helm.deployment.HelmProcessor Maven / Gradle / Ivy

There is a newer version: 1.2.6
Show newest version
package io.quarkiverse.helm.deployment;

import static io.quarkiverse.helm.deployment.HelmChartUploader.pushToHelmRepository;
import static io.quarkiverse.helm.deployment.utils.SystemPropertiesUtils.getPropertyFromSystem;
import static io.quarkiverse.helm.deployment.utils.SystemPropertiesUtils.getSystemProperties;
import static io.quarkiverse.helm.deployment.utils.SystemPropertiesUtils.hasSystemProperties;
import static io.quarkus.deployment.Capability.OPENSHIFT;
import static org.apache.commons.lang3.StringUtils.EMPTY;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Scanner;
import java.util.Set;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.config.ConfigValue;
import org.jboss.logging.Logger;

import io.dekorate.ConfigReference;
import io.dekorate.Session;
import io.dekorate.kubernetes.config.ContainerBuilder;
import io.dekorate.kubernetes.decorator.AddInitContainerDecorator;
import io.dekorate.project.Project;
import io.quarkiverse.helm.deployment.decorators.LowPriorityAddEnvVarDecorator;
import io.quarkiverse.helm.deployment.rules.ConfigReferenceStrategyManager;
import io.quarkiverse.helm.deployment.utils.HelmConfigUtils;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.ApplicationInfoBuildItem;
import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem;
import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
import io.quarkus.deployment.util.FileUtil;
import io.quarkus.kubernetes.spi.ConfiguratorBuildItem;
import io.quarkus.kubernetes.spi.DecoratorBuildItem;
import io.quarkus.kubernetes.spi.DekorateOutputBuildItem;
import io.quarkus.kubernetes.spi.GeneratedKubernetesResourceBuildItem;

public class HelmProcessor {
    private static final Logger LOGGER = Logger.getLogger(HelmProcessor.class);

    private static final String NAME_FORMAT_REG_EXP = "[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*";
    private static final List HELM_INVALID_CHARACTERS = Arrays.asList("-");
    private static final String BUILD_TIME_PROPERTIES = "/build-time-list";
    private static final String INIT_CONTAINER_CONDITION_FORMAT = "$(env | grep %s | grep -q false) && exit 0; %s";

    private static final String QUARKUS_KUBERNETES_NAME = "quarkus.kubernetes.name";
    private static final String QUARKUS_KNATIVE_NAME = "quarkus.knative.name";
    private static final String QUARKUS_OPENSHIFT_NAME = "quarkus.openshift.name";
    private static final String QUARKUS_CONTAINER_IMAGE_NAME = "quarkus.container-image.name";
    private static final String SERVICE_NAME_PLACEHOLDER = "::service-name";
    private static final String SERVICE_PORT_PLACEHOLDER = "::service-port";
    private static final String SPLIT = ":";
    private static final String PROPERTIES_CONFIG_SOURCE = "PropertiesConfigSource";
    // Lazy loaded when calling `isBuildTimeProperty(xxx)`.
    private static Set buildProperties;

    @BuildStep(onlyIf = { HelmEnabled.class, IsNormal.class })
    void mapSystemPropertiesIfEnabled(Capabilities capabilities, ApplicationInfoBuildItem info, HelmChartConfig helmConfig,
            BuildProducer decorators) {
        if (helmConfig.mapSystemProperties()) {
            String deploymentName = getDeploymentName(capabilities, info);
            Config config = ConfigProvider.getConfig();
            Map propertiesFromConfigSource = new HashMap<>();
            for (String propName : config.getPropertyNames()) {
                ConfigValue propValue = config.getConfigValue(propName);
                if (isPropertiesConfigSource(propValue.getSourceName())) {
                    propertiesFromConfigSource.put(propName, propValue.getRawValue());
                }
            }

            for (Map.Entry entry : propertiesFromConfigSource.entrySet()) {
                if (!isBuildTimeProperty(entry.getKey())) {
                    mapProperty(deploymentName, decorators, entry.getValue(), propertiesFromConfigSource);
                }
            }
        }
    }

    @BuildStep(onlyIf = { HelmEnabled.class, IsNormal.class })
    void configureHelmDependencyOrder(Capabilities capabilities, ApplicationInfoBuildItem info, HelmChartConfig config,
            BuildProducer decorators) {
        if (config.dependencies() == null || config.dependencies().isEmpty()) {
            return;
        }

        String deploymentName = getDeploymentName(capabilities, info);

        for (Map.Entry entry : config.dependencies().entrySet()) {
            HelmDependencyConfig dependency = entry.getValue();
            if (dependency.waitForService().isPresent()) {
                String containerName = "wait-for-" + defaultString(dependency.name(), entry.getKey());
                ContainerBuilder container = new ContainerBuilder()
                        .withName(containerName)
                        .withImage(dependency.waitForServiceImage())
                        .withCommand("sh");

                String argument = null;

                String service = dependency.waitForService().get();
                if (service.contains(SPLIT)) {
                    // it's service name and service port
                    String[] parts = service.split(SPLIT);
                    String serviceName = parts[0];
                    String servicePort = parts[1];
                    argument = dependency.waitForServicePortCommandTemplate()
                            .replaceAll(SERVICE_NAME_PLACEHOLDER, serviceName)
                            .replaceAll(SERVICE_PORT_PLACEHOLDER, servicePort);

                } else {
                    argument = dependency.waitForServiceOnlyCommandTemplate()
                            .replaceAll(SERVICE_NAME_PLACEHOLDER, service);
                }

                // if the condition is set, we need to map it as env property as well
                if (dependency.condition().isPresent()) {
                    String property = HelmConfigUtils.deductProperty(config, dependency.condition().get());
                    decorators.produce(new DecoratorBuildItem(
                            new LowPriorityAddEnvVarDecorator(deploymentName, containerName, property, "true")));

                    argument = String.format(INIT_CONTAINER_CONDITION_FORMAT, property, argument);
                }

                decorators.produce(new DecoratorBuildItem(
                        new AddInitContainerDecorator(deploymentName, container.withArguments("-c", argument).build())));
            }
        }
    }

    @BuildStep(onlyIf = { HelmEnabled.class, IsNormal.class })
    void generateResources(ApplicationInfoBuildItem app, OutputTargetBuildItem outputTarget,
            Optional dekorateOutput,
            List generatedResources,
            // this is added to ensure that the build step will be run
            BuildProducer dummy,
            HelmChartConfig config) {
        if (dekorateOutput.isPresent()) {
            doGenerateResources(app, outputTarget, dekorateOutput.get(), generatedResources, config);
        } else if (config.enabled()) {
            LOGGER.warn("Quarkus Helm extension is skipped since no Quarkus Kubernetes extension is configured. ");
        }
    }

    @BuildStep
    void disableDefaultHelmListener(BuildProducer helmConfiguration) {
        helmConfiguration.produce(new ConfiguratorBuildItem(new DisableDefaultHelmListener()));
    }

    private void doGenerateResources(ApplicationInfoBuildItem app, OutputTargetBuildItem outputTarget,
            DekorateOutputBuildItem dekorateOutput,
            List generatedResources,
            HelmChartConfig config) {
        validate(config);
        Project project = (Project) dekorateOutput.getProject();

        // Deduct folders
        Path inputFolder = getInputDirectory(config, project);
        Path outputFolder = getOutputDirectory(config, outputTarget);

        // Dekorate session writer
        final QuarkusHelmWriterSessionListener helmWriter = new QuarkusHelmWriterSessionListener();
        final Map> deploymentTargets = toDeploymentTargets(dekorateOutput.getGeneratedFiles(),
                generatedResources);

        // Deduct deployment target to push
        String deploymentTargetToPush = deductDeploymentTarget(config, deploymentTargets);

        // separate generated helm charts into the deployment targets
        for (Map.Entry> filesInDeploymentTarget : deploymentTargets.entrySet()) {
            String deploymentTarget = filesInDeploymentTarget.getKey();
            Path chartOutputFolder = outputFolder.resolve(deploymentTarget);
            deleteOutputHelmFolderIfExists(chartOutputFolder);

            Map generated = helmWriter.writeHelmFiles(
                    config.name().orElse(app.getName()),
                    project,
                    config,
                    getConfigReferencesFromSession(deploymentTarget, dekorateOutput),
                    inputFolder,
                    chartOutputFolder,
                    filesInDeploymentTarget.getValue());

            // Push to Helm repository if enabled
            if (config.repository().push() && deploymentTargetToPush.equals(deploymentTarget)) {
                String tarball = generated.keySet().stream()
                        .filter(file -> file.endsWith(config.extension()))
                        .findFirst()
                        .orElseThrow(() -> new RuntimeException("Couldn't find the tarball file. There should have "
                                + "been generated when pushing to a Helm repository is enabled."));
                pushToHelmRepository(new File(tarball), config.repository());
            }
        }
    }

    private void validate(HelmChartConfig config) {
        if (config.name().isPresent()) {
            if (!config.name().get().matches(NAME_FORMAT_REG_EXP)) {
                throw new IllegalStateException(String.format("Wrong name '%s'. Regular expression used for validation "
                        + "is '%s'", config.name().get(), NAME_FORMAT_REG_EXP));
            }
        }

        for (Map.Entry addIfStatement : config.addIfStatement().entrySet()) {
            String name = addIfStatement.getValue().property().orElse(addIfStatement.getKey());
            if (addIfStatement.getValue().onResourceKind().isEmpty() && addIfStatement.getValue().onResourceName().isEmpty()) {
                throw new IllegalStateException(String.format("Either 'quarkus.helm.add-if-statement.%s.on-resource-kind' "
                        + "or 'quarkus.helm.add-if-statement.%s.on-resource-kind' must be provided.",
                        addIfStatement.getKey(), addIfStatement.getKey()));
            }

            if (!config.disableNamingValidation() && HELM_INVALID_CHARACTERS.stream().anyMatch(name::contains)) {
                throw new RuntimeException(
                        String.format("The property of the `add-if-statement` '%s' is invalid. Can't use '-' characters."
                                + "You can disable the naming validation using "
                                + "`quarkus.helm.disable-naming-validation=true`", name));
            }
        }

        if (!config.disableNamingValidation()) {
            for (Map.Entry dependency : config.dependencies().entrySet()) {
                String name = dependency.getValue().name().orElse(dependency.getKey());
                if (dependency.getValue().condition().isPresent()
                        && HELM_INVALID_CHARACTERS.stream().anyMatch(dependency.getValue().condition().get()::contains)) {
                    throw new RuntimeException(
                            String.format("Condition of the dependency '%s' is invalid. Can't use '-' characters."
                                    + "You can disable the naming validation using "
                                    + "`quarkus.helm.disable-naming-validation=true`", name));
                }
            }

            for (Map.Entry value : config.values().entrySet()) {
                String name = value.getValue().property().orElse(value.getKey());
                if (HELM_INVALID_CHARACTERS.stream().anyMatch(name::contains)) {
                    throw new RuntimeException(
                            String.format("Property of the value '%s' is invalid. Can't use '-' characters."
                                    + "You can disable the naming validation using "
                                    + "`quarkus.helm.disable-naming-validation=true`", name));
                }
            }
        }
    }

    private String deductDeploymentTarget(HelmChartConfig config, Map> deploymentTargets) {
        if (config.repository().push()) {
            // if enabled, use the deployment target from the user if set
            if (config.repository().deploymentTarget().isPresent()) {
                return config.repository().deploymentTarget().get();
            } else {
                List deploymentTargetNames = deploymentTargets.keySet().stream().collect(Collectors.toList());
                if (deploymentTargetNames.size() == 1) {
                    return deploymentTargetNames.get(0);
                } else {
                    throw new IllegalStateException("Multiple deployment target found: '"
                            + deploymentTargetNames.stream().collect(
                                    Collectors.joining(", "))
                            + "'. To push the Helm Chart to the repository, "
                            + "you need to select only one using the property `quarkus.helm.repository.deployment-target`");
                }
            }
        }

        return null;
    }

    private void deleteOutputHelmFolderIfExists(Path outputFolder) {
        try {
            FileUtil.deleteIfExists(outputFolder);
        } catch (IOException ignored) {

        }
    }

    private Path getInputDirectory(HelmChartConfig config, Project project) {
        Path path = Paths.get(config.inputDirectory());
        if (!path.isAbsolute()) {
            return project.getRoot().resolve(path);
        }

        return path;
    }

    private Path getOutputDirectory(HelmChartConfig config, OutputTargetBuildItem outputTarget) {
        Path path = Paths.get(config.outputDirectory());
        if (!path.isAbsolute()) {
            return outputTarget.getOutputDirectory().resolve(path);
        }

        return path;
    }

    private Map> toDeploymentTargets(List generatedFiles,
            List generatedResources) {
        Map> filesByDeploymentTarget = new HashMap<>();
        for (String generatedFile : generatedFiles) {
            if (generatedFile.toLowerCase(Locale.ROOT).endsWith(".json")) {
                // skip json files
                continue;
            }

            File file = new File(generatedFile);
            String deploymentTarget = file.getName().substring(0, file.getName().indexOf("."));
            if (filesByDeploymentTarget.containsKey(deploymentTarget)) {
                // It's already included.
                continue;
            }

            Set files = new HashSet<>();
            if (!file.exists()) {
                Optional content = generatedResources.stream()
                        .filter(resource -> file.getName().equals(resource.getName()))
                        .map(GeneratedKubernetesResourceBuildItem::getContent)
                        .findFirst();
                if (content.isPresent()) {
                    // The dekorate output generated files are sometimes not persisted yet, so we need to workaround it by
                    // creating a temp file with the content from generatedResources.
                    try {
                        File tempFile = File.createTempFile("tmp", file.getName());
                        tempFile.deleteOnExit();
                        Files.write(tempFile.toPath(), content.get());
                        files.add(tempFile);
                    } catch (IOException ignored) {
                        // if we could not create the temp file, we add the one from
                        files.add(file);
                    }
                }
            } else {
                files.add(file);
            }

            filesByDeploymentTarget.put(deploymentTarget, files);
        }

        return filesByDeploymentTarget;
    }

    private String defaultString(Optional value, String defaultStr) {
        if (value.isEmpty() || StringUtils.isEmpty(value.get())) {
            return defaultStr;
        }

        return value.get();
    }

    private String mapProperty(String deploymentName, BuildProducer decorators, String property,
            Map propertiesFromConfigSource) {
        if (!hasSystemProperties(property)) {
            return property;
        }

        String lastPropertyValue = property;
        for (String systemProperty : getSystemProperties(property)) {
            String defaultValue = EMPTY;
            if (systemProperty.contains(SPLIT)) {
                int splitPosition = systemProperty.indexOf(SPLIT);
                defaultValue = systemProperty.substring(splitPosition + SPLIT.length());
                systemProperty = systemProperty.substring(0, splitPosition);

                if (hasSystemProperties(defaultValue)) {
                    defaultValue = mapProperty(deploymentName, decorators, defaultValue, propertiesFromConfigSource);
                }
            }

            // Incorporate if and only if the system property name is valid in Helm and it's not already defined in the
            // application properties.
            if (!propertiesFromConfigSource.containsKey(systemProperty)
                    && HELM_INVALID_CHARACTERS.stream().noneMatch(systemProperty::contains)) {
                // Check whether the system property is provided:
                defaultValue = getPropertyFromSystem(systemProperty, defaultValue);

                decorators.produce(new DecoratorBuildItem(
                        new LowPriorityAddEnvVarDecorator(deploymentName, systemProperty, defaultValue)));

                lastPropertyValue = defaultValue;
            }
        }

        return lastPropertyValue;
    }

    public static String getDeploymentName(Capabilities capabilities, ApplicationInfoBuildItem info) {
        Config config = ConfigProvider.getConfig();
        Optional resourceName;
        if (capabilities.isPresent(OPENSHIFT)) {
            resourceName = config.getOptionalValue(QUARKUS_OPENSHIFT_NAME, String.class);
        } else {
            resourceName = config.getOptionalValue(QUARKUS_KNATIVE_NAME, String.class)
                    .or(() -> config.getOptionalValue(QUARKUS_KUBERNETES_NAME, String.class));
        }

        return resourceName
                .or(() -> config.getOptionalValue(QUARKUS_CONTAINER_IMAGE_NAME, String.class))
                .orElse(info.getName());
    }

    private boolean isPropertiesConfigSource(String sourceName) {
        return StringUtils.isNotEmpty(sourceName) && sourceName.startsWith(PROPERTIES_CONFIG_SOURCE);
    }

    private boolean isBuildTimeProperty(String name) {
        if (buildProperties == null) {
            buildProperties = new HashSet<>();
            try {
                Scanner scanner = new Scanner(HelmProcessor.class.getResourceAsStream(BUILD_TIME_PROPERTIES));
                while (scanner.hasNextLine()) {
                    buildProperties.add(scanner.nextLine());
                }
            } catch (Exception e) {
                LOGGER.debugf("Can't read the build time properties file at '%s'. Caused by: %s",
                        BUILD_TIME_PROPERTIES,
                        e.getMessage());
            }
        }

        return buildProperties.stream().anyMatch(build -> name.matches(build) // It's a regular expression
                || (build.endsWith(".") && name.startsWith(build)) // contains with
                || name.equals(build)); // or it's equal to
    }

    private List getConfigReferencesFromSession(String deploymentTarget,
            DekorateOutputBuildItem dekorateOutput) {
        List configReferencesFromDecorators = ((Session) dekorateOutput.getSession())
                .getResourceRegistry()
                .getConfigReferences(deploymentTarget)
                .stream()
                .flatMap(decorator -> decorator.getConfigReferences().stream())
                // This should not be necessary, but sometimes config references from the session are not well-defined.
                .map(ConfigReferenceStrategyManager::visit)
                .collect(Collectors.toList());

        Collections.reverse(configReferencesFromDecorators);
        return configReferencesFromDecorators;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy