io.fabric8.maven.plugin.mojo.build.HelmMojo.java Source code

Java tutorial

Introduction

Here is the source code for io.fabric8.maven.plugin.mojo.build.HelmMojo.java

Source

/*
 * Copyright 2016 Red Hat, Inc.
 *
 * Red Hat licenses this file to you under the Apache License, version
 * 2.0 (the "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.  See the License for the specific language governing
 * permissions and limitations under the License.
 */
package io.fabric8.maven.plugin.mojo.build;

import java.io.File;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.io.Files;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.KubernetesList;
import io.fabric8.kubernetes.api.model.KubernetesResource;
import io.fabric8.kubernetes.client.utils.IOHelpers;
import io.fabric8.maven.core.config.HelmConfig;
import io.fabric8.maven.core.util.FileUtil;
import io.fabric8.maven.core.util.MavenUtil;
import io.fabric8.maven.core.util.ResourceFileType;
import io.fabric8.maven.core.util.ResourceUtil;
import io.fabric8.maven.core.util.kubernetes.Fabric8Annotations;
import io.fabric8.maven.core.util.kubernetes.KubernetesHelper;
import io.fabric8.maven.core.util.kubernetes.KubernetesResourceUtil;
import io.fabric8.maven.plugin.mojo.AbstractFabric8Mojo;
import io.fabric8.openshift.api.model.Template;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.model.Developer;
import org.apache.maven.model.Scm;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.project.MavenProjectHelper;
import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.tar.TarArchiver;

/**
 * Generates a Helm chart for the kubernetes resources
 */
@Mojo(name = "helm", defaultPhase = LifecyclePhase.PRE_INTEGRATION_TEST)
public class HelmMojo extends AbstractFabric8Mojo {
    protected static final String FAILED_TO_LOAD_KUBERNETES_YAML = "Failed to load kubernetes YAML ";
    protected static final String YAML_EXTENSION = ".yaml";

    @Parameter
    private HelmConfig helm;

    /**
     * The generated kubernetes YAML file
     */
    @Parameter(property = "fabric8.kubernetesManifest", defaultValue = "${basedir}/target/classes/META-INF/fabric8/kubernetes.yml")
    private File kubernetesManifest;

    /**
     * The generated kubernetes YAML file
     */
    @Parameter(property = "fabric8.kubernetesTemplate", defaultValue = "${basedir}/target/classes/META-INF/fabric8/k8s-template.yml")
    private File kubernetesTemplate;

    @Component
    private MavenProjectHelper projectHelper;

    @Component(role = Archiver.class, hint = "tar")
    private TarArchiver archiver;

    @Override
    public void executeInternal() throws MojoExecutionException, MojoFailureException {
        String chartName = getChartName();

        for (HelmConfig.HelmType type : getHelmTypes()) {
            generateHelmChartDirectory(chartName, type);
        }
    }

    protected void generateHelmChartDirectory(String chartName, HelmConfig.HelmType type)
            throws MojoExecutionException {
        File outputDir = prepareOutputDir(type);
        File sourceDir = checkSourceDir(chartName, type);
        if (sourceDir == null) {
            return;
        }
        log.info("Creating Helm Chart \"%s\" for %s", chartName, type.getDescription());
        log.verbose("SourceDir: %s", sourceDir);
        log.verbose("OutputDir: %s", outputDir);

        // Copy over all resource descriptors into the helm templates dir
        File templatesDir = copyResourceFilesToTemplatesDir(outputDir, sourceDir);

        // Save Helm chart
        createChartYaml(chartName, outputDir);

        // Copy over support files
        copyTextFile(outputDir, "README");
        copyTextFile(outputDir, "LICENSE");

        Template template = findTemplate();
        if (template != null) {
            createTemplateParameters(outputDir, template, templatesDir);
        }
        // now lets create the tarball
        File destinationFile = new File(project.getBuild().getDirectory(), chartName + "-" + project.getVersion()
                + "-" + type.getClassifier() + "." + getChartFileExtension());
        MavenUtil.createArchive(outputDir.getParentFile(), destinationFile, this.archiver);
        projectHelper.attachArtifact(project, getChartFileExtension(), type.getClassifier(), destinationFile);
    }

    private String getChartName() {
        String ret = getProperty("fabric8.helm.chart");
        if (ret != null) {
            return ret;
        }
        if (helm != null) {
            ret = helm.getChart();
        }
        return ret != null ? ret : project.getArtifactId();
    }

    private String getChartFileExtension() {
        String ret = getProperty("fabric8.helm.chartExtension");
        if (ret != null) {
            return ret;
        }
        if (helm != null) {
            ret = helm.getChartExtension();
        }
        return ret != null ? ret : "tar.gz";
    }

    private File prepareOutputDir(HelmConfig.HelmType type) throws MojoExecutionException {
        String dir = getProperty("fabric8.helm.outputDir");
        if (dir == null) {
            dir = String.format("%s/fabric8/helm/%s/%s", project.getBuild().getDirectory(), type.getOutputDir(),
                    getChartName());
        }
        File dirF = new File(dir);
        try {
            if (dirF.isFile()) {
                FileUtils.deleteDirectory(dirF);
            }
            return dirF;
        } catch (IOException e) {
            throw new MojoExecutionException("Cannot delete directory " + dir + ": " + e, e);
        }
    }

    private File checkSourceDir(String chartName, HelmConfig.HelmType type) {
        String dir = getProperty("fabric8.helm.sourceDir");
        if (dir == null) {
            dir = project.getBuild().getOutputDirectory() + "/META-INF/fabric8/" + type.getSourceDir();
        }
        File dirF = new File(dir);
        if (!dirF.isDirectory() || !dirF.exists()) {
            log.warn("Chart source directory %s does not exist so cannot make chart %s. "
                    + "Probably you need run 'mvn fabric8:resource' before.", dirF, chartName);
            return null;
        }
        if (!containsYamlFiles(dirF)) {
            log.warn("Chart source directory %s does not contain any YAML manifest to make chart %s. "
                    + "Probably you need run 'mvn fabric8:resource' before.", dirF, chartName);
            return null;
        }
        return dirF;
    }

    private List<HelmConfig.HelmType> getHelmTypes() {
        String helmTypeProp = getProperty("fabric8.helm.type");
        if (StringUtils.isNotBlank(helmTypeProp)) {
            String[] propTypes = StringUtils.split(helmTypeProp, ",");
            List<HelmConfig.HelmType> ret = new ArrayList<>();
            for (String prop : propTypes) {
                ret.add(HelmConfig.HelmType.valueOf(prop.trim().toLowerCase()));
            }
            return ret;
        }
        if (helm != null) {
            List<HelmConfig.HelmType> types = helm.getType();
            if (types != null && types.size() > 0) {
                return types;
            }
        }
        return Arrays.asList(HelmConfig.HelmType.kubernetes);
    }

    private void createChartYaml(String chartName, File outputDir) throws MojoExecutionException {
        Chart chart = helm != null ? new Chart(chartName, project, helm.getKeywords(), helm.getEngine())
                : new Chart(chartName, project);

        String iconUrl = findIconURL();
        getLog().debug("Found icon: " + iconUrl);
        if (StringUtils.isNotBlank(iconUrl)) {
            chart.setIcon(iconUrl);
        }
        File outputChartFile = new File(outputDir, "Chart.yaml");
        try {
            ResourceUtil.save(outputChartFile, chart, ResourceFileType.yaml);
        } catch (IOException e) {
            throw new MojoExecutionException("Failed to save chart " + outputChartFile + ": " + e, e);
        }
    }

    private void createTemplateParameters(File outputDir, Template template, File templatesDir)
            throws MojoExecutionException {
        JsonNodeFactory nodeFactory = JsonNodeFactory.instance;
        ObjectNode values = nodeFactory.objectNode();
        List<io.fabric8.openshift.api.model.Parameter> parameters = template.getParameters();
        if (parameters == null || parameters.isEmpty()) {
            return;
        }
        List<HelmParameter> helmParameters = new ArrayList<>();
        for (io.fabric8.openshift.api.model.Parameter parameter : parameters) {
            HelmParameter helmParameter = new HelmParameter(parameter);
            helmParameter.addToValue(values);
            helmParameters.add(helmParameter);
        }
        File outputChartFile = new File(outputDir, "values.yaml");
        try {
            ResourceUtil.save(outputChartFile, values, ResourceFileType.yaml);
        } catch (IOException e) {
            throw new MojoExecutionException("Failed to save chart values " + outputChartFile + ": " + e, e);
        }

        // now lets replace all the parameter expressions in each template
        File[] files = templatesDir.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isFile()) {
                    String extension = Files.getFileExtension(file.getName()).toLowerCase();
                    if (extension.equals("yaml") || extension.equals("yml")) {
                        convertTemplateParameterExpressionsWithHelmExpressions(file, helmParameters);
                    }
                }
            }
        }
    }

    private void convertTemplateParameterExpressionsWithHelmExpressions(File file,
            List<HelmParameter> helmParameters) throws MojoExecutionException {
        String text = null;
        try {
            text = FileUtils.readFileToString(file, Charset.defaultCharset());
        } catch (IOException e) {
            throw new MojoExecutionException(
                    "Failed to load " + file + " so we can replacing template expressions " + e, e);
        }
        String original = text;
        for (HelmParameter helmParameter : helmParameters) {
            text = helmParameter.convertTemplateParameterToHelmExpression(text);
        }
        if (!original.equals(text)) {
            try {
                FileUtils.writeStringToFile(file, text, Charset.defaultCharset());
            } catch (IOException e) {
                throw new MojoExecutionException(
                        "Failed to save " + file + " after replacing template expressions " + e, e);
            }
        }
    }

    private String findIconURL() throws MojoExecutionException {
        String answer = null;
        if (kubernetesManifest != null && kubernetesManifest.isFile()) {
            Object dto = null;
            try {
                dto = ResourceUtil.load(kubernetesManifest, KubernetesResource.class);
            } catch (IOException e) {
                throw new MojoExecutionException("Failed to load kubernetes YAML " + kubernetesManifest + ". " + e,
                        e);
            }
            if (dto instanceof HasMetadata) {
                answer = KubernetesHelper.getOrCreateAnnotations((HasMetadata) dto)
                        .get(Fabric8Annotations.ICON_URL.value());
            }
            if (StringUtils.isBlank(answer) && dto instanceof KubernetesList) {
                KubernetesList list = (KubernetesList) dto;
                List<HasMetadata> items = list.getItems();
                if (items != null) {
                    for (HasMetadata item : items) {
                        answer = KubernetesHelper.getOrCreateAnnotations(item)
                                .get(Fabric8Annotations.ICON_URL.value());
                        if (StringUtils.isNotBlank(answer)) {
                            break;
                        }
                    }
                }
            }
        } else {
            getLog().warn("No kubernetes manifest file has been generated yet by the fabric8:resource goal at: "
                    + kubernetesManifest);
        }
        return answer;
    }

    private Template findTemplate() throws MojoExecutionException {
        if (kubernetesTemplate != null && kubernetesTemplate.isFile()) {
            Object dto = null;
            try {
                dto = ResourceUtil.load(kubernetesTemplate, KubernetesResource.class, ResourceFileType.yaml);
            } catch (IOException e) {
                throw new MojoExecutionException("Failed to load kubernetes YAML " + kubernetesTemplate + ". " + e,
                        e);
            }
            if (dto instanceof Template) {
                return (Template) dto;
            }
            if (dto instanceof KubernetesList) {
                KubernetesList list = (KubernetesList) dto;
                List<HasMetadata> items = list.getItems();
                if (items != null) {
                    for (HasMetadata item : items) {
                        if (item instanceof Template) {
                            return (Template) item;
                        }
                    }
                }
            }
        }
        return null;
    }

    private File copyResourceFilesToTemplatesDir(File outputDir, File sourceDir) throws MojoExecutionException {
        File templatesDir = new File(outputDir, "templates");
        templatesDir.mkdirs();
        File[] files = sourceDir.listFiles();
        if (files != null) {
            for (File file : files) {
                Object dto;
                try {
                    dto = ResourceUtil.load(file, KubernetesResource.class, ResourceFileType.yaml);
                } catch (IOException e) {
                    throw new MojoExecutionException(FAILED_TO_LOAD_KUBERNETES_YAML + file + ". " + e, e);
                }
                if (dto instanceof Template) {
                    // lets split the template into separate files!
                    Template template = (Template) dto;
                    copyTemplateResourcesToTemplatesDir(templatesDir, template);
                    continue;
                }

                String name = file.getName();
                if (name.endsWith(".yml")) {
                    name = FileUtil.stripPostfix(name, ".yml") + YAML_EXTENSION;
                }
                File targetFile = new File(templatesDir, name);
                try {
                    // lets escape any {{ or }} characters to avoid creating invalid templates
                    String text = FileUtils.readFileToString(file, Charset.defaultCharset());
                    text = escapeYamlTemplate(text);
                    FileUtils.write(targetFile, text, Charset.defaultCharset());
                } catch (IOException e) {
                    throw new MojoExecutionException(
                            "Failed to copy manifest files from " + file + " to " + targetFile + ": " + e, e);
                }
            }
        }
        return templatesDir;
    }

    private void copyTemplateResourcesToTemplatesDir(File templatesDir, Template template)
            throws MojoExecutionException {
        List<HasMetadata> objects = template.getObjects();
        if (objects != null) {
            for (HasMetadata object : objects) {
                String name = KubernetesResourceUtil.getNameWithSuffix(KubernetesHelper.getName(object),
                        KubernetesHelper.getKind(object)) + ".yaml";
                File outFile = new File(templatesDir, name);
                try {
                    ResourceUtil.save(outFile, object);
                } catch (IOException e) {
                    throw new MojoExecutionException("Failed to save template " + outFile + ": " + e, e);
                }
            }
        }
    }

    public static String escapeYamlTemplate(String template) {
        StringBuffer answer = new StringBuffer();
        int count = 0;
        char last = 0;
        for (int i = 0, size = template.length(); i < size; i++) {
            char ch = template.charAt(i);
            if (ch == '{' || ch == '}') {
                if (count == 0) {
                    last = ch;
                    count = 1;
                } else {
                    if (ch == last) {
                        answer.append(ch == '{' ? "{{\"{{\"}}" : "{{\"}}\"}}");
                    } else {
                        answer.append(last);
                        answer.append(ch);
                    }
                    count = 0;
                    last = 0;
                }
            } else {
                if (count > 0) {
                    answer.append(last);
                }
                answer.append(ch);
                count = 0;
                last = 0;
            }
        }
        if (count > 0) {
            answer.append(last);
        }
        return answer.toString();
    }

    private boolean containsYamlFiles(File sourceDir) {
        File[] files = sourceDir.listFiles();
        if (files != null) {
            for (File file : files) {
                String lower = file.getName().toLowerCase();
                if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
                    return true;
                }
            }
        }
        return false;
    }

    private void copyTextFile(File outputDir, final String srcFile) throws MojoExecutionException {
        try {
            FilenameFilter filter = new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    String lower = name.toLowerCase(Locale.ENGLISH);
                    return lower.equals(srcFile.toLowerCase()) || lower.startsWith(srcFile.toLowerCase() + ".");
                }
            };
            copyFirstFile(project.getBasedir(), filter, outputDir);
        } catch (IOException e) {
            throw new MojoExecutionException("Failed to save " + srcFile + ": " + e, e);
        }
    }

    protected void copyFirstFile(File sourceDir, FilenameFilter filter, File outDir) throws IOException {
        File[] files = sourceDir.listFiles(filter);
        if (files != null && files.length > 0) {
            File sourceFile = files[0];
            FileUtils.copyFile(sourceFile, new File(outDir, sourceFile.getName()));
        }
        if (files != null && files.length > 1) {
            log.warn("Found %d of %s files. Using first one %s", files.length, files[0].getName(), files[0]);
        }
    }

    // =================================================================================================================
    /**
     * Represents the <a href="https://github.com/kubernetes/helm">Helm</a>
     * <a href="https://github.com/kubernetes/helm/blob/master/pkg/proto/hapi/chart/metadata.pb.go#L50">Chart.yaml file</a>
     */
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    public static class Chart {
        @JsonProperty
        private String name;
        @JsonProperty
        private String home;
        @JsonProperty
        private List<String> sources;
        @JsonProperty
        private String version;
        @JsonProperty
        private String description;
        @JsonProperty
        private List<String> keywords;
        @JsonProperty
        private List<Maintainer> maintainers;
        @JsonProperty
        private String engine;
        @JsonProperty
        private String icon;

        public Chart() {
        }

        public Chart(String name, String version) {
            this.name = name;
            this.version = version;
        }

        public Chart(String name, MavenProject project) {
            this(name, project, null, null);
        }

        public Chart(String name, MavenProject project, List<String> keywords, String engine) {
            this.name = name;
            this.keywords = keywords;
            this.engine = engine;

            this.name = name;
            if (project != null) {
                this.version = project.getVersion();
                this.description = project.getDescription();
                this.home = project.getUrl();
                this.keywords = keywords;
                this.engine = engine;

                Scm scm = project.getScm();
                if (scm != null) {
                    String url = scm.getUrl();
                    if (url != null) {
                        List<String> sources1 = new ArrayList<>();
                        sources1.add(url);
                        this.sources = sources1;
                    }
                }
                List<Developer> developers = project.getDevelopers();
                if (developers != null) {
                    List<Maintainer> maintainers1 = new ArrayList<>();
                    for (Developer developer : developers) {
                        String email = developer.getEmail();
                        String devName = developer.getName();
                        if (StringUtils.isNotBlank(devName) || StringUtils.isNotBlank(email)) {
                            Maintainer maintainer = new Maintainer(devName, email);
                            maintainers1.add(maintainer);
                        }
                    }
                    this.maintainers = maintainers1;
                }
            }
        }

        @Override
        public String toString() {
            return "Chart{" + "name='" + name + '\'' + ", home='" + home + '\'' + ", version='" + version + '\''
                    + '}';
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getHome() {
            return home;
        }

        public void setHome(String home) {
            this.home = home;
        }

        public List<String> getSources() {
            return sources;
        }

        public void setSources(List<String> sources) {
            this.sources = sources;
        }

        public String getVersion() {
            return version;
        }

        public void setVersion(String version) {
            this.version = version;
        }

        public String getDescription() {
            return description;
        }

        public void setDescription(String description) {
            this.description = description;
        }

        public List<String> getKeywords() {
            return keywords;
        }

        public void setKeywords(List<String> keywords) {
            this.keywords = keywords;
        }

        public List<Maintainer> getMaintainers() {
            return maintainers;
        }

        public void setMaintainers(List<Maintainer> maintainers) {
            this.maintainers = maintainers;
        }

        public String getEngine() {
            return engine;
        }

        public void setEngine(String engine) {
            this.engine = engine;
        }

        public String getIcon() {
            return icon;
        }

        public void setIcon(String icon) {
            this.icon = icon;
        }

        /**
         */
        @JsonInclude(JsonInclude.Include.NON_EMPTY)
        public static class Maintainer {

            @JsonProperty
            private String name;

            @JsonProperty
            private String email;

            public Maintainer() {
            }

            public Maintainer(String name, String email) {
                this.name = name;
                this.email = email;
            }

            public String getName() {
                return name;
            }

            public void setName(String name) {
                this.name = name;
            }

            public String getEmail() {
                return email;
            }

            public void setEmail(String email) {
                this.email = email;
            }
        }
    }

    public static class HelmParameter {
        private final io.fabric8.openshift.api.model.Parameter parameter;
        private final String helmName;

        public HelmParameter(io.fabric8.openshift.api.model.Parameter parameter) {
            this.parameter = parameter;
            this.helmName = parameter.getName().toLowerCase();
        }

        public void addToValue(ObjectNode values) {
            String value = parameter.getValue();
            if (value != null) {
                values.put(helmName, value);
            }
        }

        public io.fabric8.openshift.api.model.Parameter getParameter() {
            return parameter;
        }

        public String getHelmName() {
            return helmName;
        }

        public String convertTemplateParameterToHelmExpression(String text) {
            String name = parameter.getName();
            String from = "${" + name + "}";
            String answer = text;
            String defaultExpression = "";
            String required = "";
            String value = parameter.getValue();
            if (value != null) {
                defaultExpression = " | default \"" + value + "\"";
            }
            Boolean flag = parameter.getRequired();
            if (flag != null && flag.booleanValue()) {
                required = "required \"A valid .Values." + helmName + " entry required!\" ";
            }
            String to = "{{ " + required + ".Values." + helmName + defaultExpression + " }}";
            answer = answer.replace(from, to);
            from = "$" + name;
            return answer.replace(from, to);
        }

    }
}