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

org.jreleaser.packagers.DockerPackagerProcessor Maven / Gradle / Ivy

The newest version!
/*
 * SPDX-License-Identifier: Apache-2.0
 *
 * Copyright 2020-2024 The JReleaser authors.
 *
 * Licensed 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
 *
 *     https://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 org.jreleaser.packagers;

import org.jreleaser.bundle.RB;
import org.jreleaser.model.internal.JReleaserContext;
import org.jreleaser.model.internal.common.Artifact;
import org.jreleaser.model.internal.distributions.Distribution;
import org.jreleaser.model.internal.packagers.DockerConfiguration;
import org.jreleaser.model.internal.packagers.DockerPackager;
import org.jreleaser.model.internal.packagers.DockerSpec;
import org.jreleaser.model.spi.packagers.PackagerProcessingException;
import org.jreleaser.mustache.TemplateContext;
import org.jreleaser.sdk.command.Command;
import org.jreleaser.util.FileUtils;
import org.jreleaser.util.PlatformUtils;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import static org.jreleaser.model.Constants.KEY_DISTRIBUTION_JAVA_MAIN_CLASS;
import static org.jreleaser.model.Constants.KEY_DISTRIBUTION_JAVA_MAIN_MODULE;
import static org.jreleaser.model.Constants.KEY_DISTRIBUTION_PACKAGE_DIRECTORY;
import static org.jreleaser.model.Constants.KEY_DISTRIBUTION_PREPARE_DIRECTORY;
import static org.jreleaser.model.Constants.KEY_DOCKER_BASE_IMAGE;
import static org.jreleaser.model.Constants.KEY_DOCKER_LABELS;
import static org.jreleaser.model.Constants.KEY_DOCKER_POST_COMMANDS;
import static org.jreleaser.model.Constants.KEY_DOCKER_PRE_COMMANDS;
import static org.jreleaser.model.Constants.KEY_DOCKER_SPEC_NAME;
import static org.jreleaser.mustache.MustacheUtils.passThrough;
import static org.jreleaser.mustache.Templates.resolveTemplate;
import static org.jreleaser.templates.TemplateUtils.trimTplExtension;
import static org.jreleaser.util.StringUtils.isNotBlank;

/**
 * @author Andres Almiray
 * @since 0.1.0
 */
public class DockerPackagerProcessor extends AbstractRepositoryPackagerProcessor {
    private static final String ROOT = "ROOT";

    public DockerPackagerProcessor(JReleaserContext context) {
        super(context);
    }

    @Override
    protected void doPrepareDistribution(Distribution distribution,
                                         TemplateContext props,
                                         String distributionName,
                                         Path prepareDirectory,
                                         String templateDirectory,
                                         String packagerName,
                                         boolean copyLicense) throws IOException, PackagerProcessingException {
        if (packager.getActiveSpecs().isEmpty()) {
            super.doPrepareDistribution(distribution, props, distributionName,
                prepareDirectory, templateDirectory, packagerName, true);

            if (!packager.isUseLocalArtifact()) {
                Files.move(prepareDirectory.resolve("Dockerfile-remote"),
                    prepareDirectory.resolve("Dockerfile"),
                    REPLACE_EXISTING);
            } else {
                Files.deleteIfExists(prepareDirectory.resolve("Dockerfile-remote"));
            }

            return;
        }

        // copy root files
        String rootTemplateDirectory = getPackager().getTemplateDirectory() + File.separator + ROOT;
        super.doPrepareDistribution(distribution, props, distributionName,
            prepareDirectory.resolve(ROOT),
            rootTemplateDirectory,
            packager.getType(),
            false);
        Files.deleteIfExists(prepareDirectory.resolve(ROOT).resolve("Dockerfile"));
        Files.deleteIfExists(prepareDirectory.resolve(ROOT).resolve("Dockerfile-remote"));

        for (DockerSpec spec : packager.getActiveSpecs()) {
            prepareSpec(distribution, props, distributionName, prepareDirectory, spec);
        }
    }

    private void prepareSpec(Distribution distribution,
                             TemplateContext props,
                             String distributionName,
                             Path prepareDirectory,
                             DockerSpec spec) throws IOException, PackagerProcessingException {
        TemplateContext newProps = fillSpecProps(distribution, props, spec);
        context.getLogger().debug(RB.$("distributions.action.preparing") + " {} spec", spec.getName());
        super.doPrepareDistribution(distribution, newProps, distributionName,
            prepareDirectory.resolve(spec.getName()),
            spec.getTemplateDirectory(),
            spec.getName() + "/" + packager.getType(),
            false);

        if (!spec.isUseLocalArtifact()) {
            Files.move(prepareDirectory.resolve(spec.getName()).resolve("Dockerfile-remote"),
                prepareDirectory.resolve(spec.getName()).resolve("Dockerfile"),
                REPLACE_EXISTING);
        } else {
            Files.deleteIfExists(prepareDirectory.resolve(spec.getName()).resolve("Dockerfile-remote"));
        }
    }

    private TemplateContext fillSpecProps(Distribution distribution, TemplateContext props, DockerSpec spec) {
        List artifacts = singletonList(spec.getArtifact());
        TemplateContext newProps = fillProps(distribution, props);
        newProps.set(KEY_DOCKER_SPEC_NAME, spec.getName());
        fillDockerProperties(newProps, spec);
        verifyAndAddArtifacts(newProps, distribution, artifacts);
        Path prepareDirectory = newProps.get(KEY_DISTRIBUTION_PREPARE_DIRECTORY);
        newProps.set(KEY_DISTRIBUTION_PREPARE_DIRECTORY, prepareDirectory.resolve(spec.getName()));
        Path packageDirectory = newProps.get(KEY_DISTRIBUTION_PACKAGE_DIRECTORY);
        newProps.set(KEY_DISTRIBUTION_PACKAGE_DIRECTORY, packageDirectory.resolve(spec.getName()));
        return newProps;
    }

    @Override
    protected boolean verifyAndAddArtifacts(TemplateContext props, Distribution distribution) {
        if (packager.getActiveSpecs().isEmpty()) {
            return super.verifyAndAddArtifacts(props, distribution);
        }
        return true;
    }

    @Override
    protected void doPackageDistribution(Distribution distribution,
                                         TemplateContext props,
                                         Path packageDirectory) throws PackagerProcessingException {
        if (packager.getActiveSpecs().isEmpty()) {
            List artifacts = packager.resolveArtifacts(context, distribution);
            packageDocker(distribution, props, packageDirectory, getPackager(), artifacts);
            return;
        }

        Path rootPrepareDirectory = getPrepareDirectory(props).resolve(ROOT);
        Path rootPackageDirectory = getPackageDirectory(props).resolve(ROOT);
        copyFiles(rootPrepareDirectory, rootPackageDirectory);

        for (DockerSpec spec : packager.getActiveSpecs()) {
            context.getLogger().debug(RB.$("distributions.action.packaging") + " {} spec", spec.getName());
            TemplateContext newProps = fillSpecProps(distribution, props, spec);
            packageDocker(distribution, newProps, packageDirectory.resolve(spec.getName()),
                spec, singletonList(spec.getArtifact()));
        }
    }

    protected void packageDocker(Distribution distribution,
                                 TemplateContext props,
                                 Path packageDirectory,
                                 DockerConfiguration docker,
                                 List artifacts) throws PackagerProcessingException {
        super.doPackageDistribution(distribution, props, packageDirectory);

        try {
            // copy files
            Path workingDirectory = prepareAssembly(distribution, props, packageDirectory, artifacts);

            Map> tagNames = resolveTagNames(docker, props);
            List tags = tagNames.values().stream()
                .flatMap(List::stream)
                .collect(toList());

            tags.forEach(tag -> context.getLogger().info(" - {}", tag));

            if (docker.getBuildx().isEnabled()) {
                // create builder if needed
                createBuildxBuilder(props, docker);
                configureAndExecuteBuildCommand(buildxBuildCommand(props, docker), workingDirectory, tags);
            } else {
                configureAndExecuteBuildCommand(buildCommand(props, docker), workingDirectory, tags);
            }
        } catch (IOException e) {
            throw new PackagerProcessingException(e);
        }
    }

    private void configureAndExecuteBuildCommand(Command cmd, Path workingDirectory, List tags) throws PackagerProcessingException {
        if (!cmd.hasArg("-q") && !cmd.hasArg("--quiet")) {
            cmd.arg("--quiet");
        }
        cmd.arg("--file");
        cmd.arg(workingDirectory.resolve("Dockerfile").toAbsolutePath().toString());
        for (String tag : tags) {
            cmd.arg("--tag");
            cmd.arg(tag);
        }
        cmd.arg(workingDirectory.toAbsolutePath().toString());
        context.getLogger().debug(String.join(" ", cmd.getArgs()));

        // execute
        executeCommand(cmd);
    }

    private void createBuildxBuilder(TemplateContext props, DockerConfiguration docker) throws PackagerProcessingException {
        if (!docker.getBuildx().isCreateBuilder()) return;

        Command cmd = new Command("docker" + (PlatformUtils.isWindows() ? ".exe" : ""))
            .arg("buildx")
            .arg("ls");
        Command.Result result = executeCommand(cmd);
        if (result.getOut().contains("jreleaser")) return;

        cmd = buildxCreateCommand(props, docker);
        context.getLogger().debug(String.join(" ", cmd.getArgs()));
        executeCommand(cmd);
    }

    private Path prepareAssembly(Distribution distribution,
                                 TemplateContext props,
                                 Path packageDirectory,
                                 List artifacts) throws IOException, PackagerProcessingException {
        copyPreparedFiles(props);
        Path assemblyDirectory = packageDirectory.resolve("assembly");

        Files.createDirectories(assemblyDirectory);

        for (Artifact artifact : artifacts) {
            Path artifactPath = artifact.getEffectivePath(context, distribution);
            if (distribution.getType() == org.jreleaser.model.Distribution.DistributionType.BINARY) {
                if (artifactPath.toString().endsWith(".zip")) {
                    FileUtils.unpackArchive(artifactPath, assemblyDirectory);
                } else {
                    Files.copy(artifactPath, assemblyDirectory.resolve(artifactPath.getFileName()), REPLACE_EXISTING);
                }
            } else {
                Files.copy(artifactPath, assemblyDirectory.resolve(artifactPath.getFileName()), REPLACE_EXISTING);
            }
        }

        return packageDirectory;
    }

    private Command buildCommand(TemplateContext props, DockerConfiguration docker) {
        Command cmd = createCommand("build");
        for (int i = 0; i < docker.getBuildArgs().size(); i++) {
            String arg = docker.getBuildArgs().get(i);
            if (arg.contains("{{")) {
                cmd.arg(resolveTemplate(arg, props).trim());
            } else {
                cmd.arg(arg.trim());
            }
        }
        return cmd;
    }

    private Command buildxBuildCommand(TemplateContext props, DockerConfiguration docker) {
        Command cmd = createCommand("buildx");
        cmd.arg("build");

        List platforms = new ArrayList<>();
        for (int i = 0; i < docker.getBuildx().getPlatforms().size(); i++) {
            String arg = docker.getBuildx().getPlatforms().get(i);
            if (arg.contains("{{")) {
                platforms.add(resolveTemplate(arg, props).trim());
            } else {
                platforms.add(arg.trim());
            }
        }
        cmd.arg("--platform")
            .arg(String.join(",", platforms));

        for (int i = 0; i < docker.getBuildArgs().size(); i++) {
            String arg = docker.getBuildArgs().get(i);
            if (arg.contains("{{")) {
                cmd.arg(resolveTemplate(arg, props).trim());
            } else {
                cmd.arg(arg.trim());
            }
        }
        return cmd;
    }

    private Command buildxCreateCommand(TemplateContext props, DockerConfiguration docker) {
        Command cmd = createCommand("buildx");
        cmd.arg("create");
        for (int i = 0; i < docker.getBuildx().getCreateBuilderFlags().size(); i++) {
            String arg = docker.getBuildx().getCreateBuilderFlags().get(i);
            if (arg.contains("{{")) {
                cmd.arg(resolveTemplate(arg, props).trim());
            } else {
                cmd.arg(arg.trim());
            }
        }
        return cmd;
    }

    private Command createCommand(String name) {
        return new Command("docker" + (PlatformUtils.isWindows() ? ".exe" : ""))
            .arg("-l")
            .arg("error")
            .arg(name);
    }

    @Override
    public void publishDistribution(Distribution distribution, TemplateContext props) throws PackagerProcessingException {
        if (packager.getActiveSpecs().isEmpty()) {
            publishToRepository(distribution, props);
            super.publishDistribution(distribution, props);
            cleanupBuilder(props, getPackager());
            return;
        }

        publishToRepository(distribution, props);
        for (DockerSpec spec : packager.getActiveSpecs()) {
            context.getLogger().debug(RB.$("distributions.action.publishing") + " {} spec", spec.getName());
            TemplateContext newProps = fillSpecProps(distribution, props, spec);
            publishDocker(newProps, spec);
        }
        cleanupBuilder(props, packager.getActiveSpecs());
    }

    private void publishToRepository(Distribution distribution, TemplateContext props) throws PackagerProcessingException {
        super.doPublishDistribution(distribution, fillProps(distribution, props));
    }

    @Override
    protected void doPublishDistribution(Distribution distribution, TemplateContext props) throws PackagerProcessingException {
        publishDocker(props, getPackager());
    }

    protected void publishDocker(TemplateContext props, DockerConfiguration docker) throws PackagerProcessingException {
        Map> tagNames = resolveTagNames(docker, props);

        if (context.isDryrun()) {
            for (Map.Entry> e : tagNames.entrySet()) {
                Set uniqueImageNames = e.getValue().stream()
                    .map(tag -> tag.split(":")[0])
                    .collect(toSet());
                for (String imageName : uniqueImageNames) {
                    context.getLogger().info(" - {}", imageName);
                }
            }
            return;
        }

        for (DockerConfiguration.Registry registry : docker.getRegistries()) {
            login(registry);
        }

        if (docker.getBuildx().isEnabled()) {
            Path workingDirectory = props.get(KEY_DISTRIBUTION_PACKAGE_DIRECTORY);
            List tags = tagNames.values().stream()
                .flatMap(List::stream)
                .collect(toList());

            // command line
            Command cmd = buildxBuildCommand(props, docker);
            if (!cmd.hasArg("-q") && !cmd.hasArg("--quiet")) {
                cmd.arg("--quiet");
            }
            cmd.arg("--file");
            cmd.arg(workingDirectory.resolve("Dockerfile").toAbsolutePath().toString());
            for (String tag : tags) {
                cmd.arg("--tag");
                cmd.arg(tag);
            }
            cmd.arg("--push");
            cmd.arg(workingDirectory.toAbsolutePath().toString());
            context.getLogger().debug(String.join(" ", cmd.getArgs()));

            // execute
            executeCommand(cmd);
        } else {
            for (Map.Entry> e : tagNames.entrySet()) {
                Set uniqueImageNames = e.getValue().stream()
                    .map(tag -> tag.split(":")[0])
                    .collect(toSet());
                for (String imageName : uniqueImageNames) {
                    push(e.getKey(), imageName);
                }
            }
        }

        for (DockerConfiguration.Registry registry : docker.getRegistries()) {
            logout(registry);
        }
    }

    private void cleanupBuilder(TemplateContext props, DockerConfiguration docker) throws PackagerProcessingException {
        if (docker.getBuildx().isEnabled() && docker.getBuildx().isCreateBuilder()) {
            int i = docker.getBuildx().getCreateBuilderFlags().indexOf("--name");
            String builderName = docker.getBuildx().getCreateBuilderFlags().get(i + 1);
            Command cmd = createCommand("buildx")
                .arg("rm")
                .arg(resolveTemplate(builderName, props).trim());

            executeCommand(cmd);
        }
    }

    private void cleanupBuilder(TemplateContext props, List specs) throws PackagerProcessingException {
        Set builderNames = new LinkedHashSet<>();
        for (DockerSpec spec : specs) {
            if (spec.getBuildx().isEnabled() && spec.getBuildx().isCreateBuilder()) {
                int i = spec.getBuildx().getCreateBuilderFlags().indexOf("--name");
                builderNames.add(spec.getBuildx().getCreateBuilderFlags().get(i + 1));
            }
        }

        for (String builderName : builderNames) {
            Command cmd = createCommand("buildx")
                .arg("rm")
                .arg(resolveTemplate(builderName, props).trim());

            executeCommand(cmd);
        }
    }

    private void login(DockerConfiguration.Registry registry) throws PackagerProcessingException {
        if (registry.isExternalLogin()) return;

        Command cmd = createCommand("login");
        if (isNotBlank(registry.getServer())) {
            cmd.arg(registry.getServer());
        }
        cmd.arg("-u");
        cmd.arg(registry.getUsername());
        cmd.arg("-p");
        cmd.arg(registry.getPassword());

        ByteArrayInputStream in = new ByteArrayInputStream((registry.getPassword() + System.lineSeparator()).getBytes(UTF_8));

        context.getLogger().debug(RB.$("docker.login"),
            registry.getServerName(),
            isNotBlank(registry.getServer()) ? " (" + registry.getServer() + ")" : "");
        if (!context.isDryrun()) executeCommand(cmd, in);
    }

    private Map> resolveTagNames(DockerConfiguration docker, TemplateContext props) {
        Map> tags = new LinkedHashMap<>();

        for (DockerConfiguration.Registry registry : docker.getRegistries()) {
            for (String imageName : docker.getImageNames()) {
                imageName = resolveTemplate(imageName, props).toLowerCase(Locale.ENGLISH);

                String tag = imageName;
                String serverName = registry.getServerName();
                String server = registry.getServer();
                String repositoryName = registry.getRepositoryName();

                // if serverName == DEFAULT
                //   tag: docker.io/repositoryName/imageName
                // else
                //   tag: server/repositoryName/imageName

                if (DockerConfiguration.Registry.DEFAULT_NAME.equals(serverName)) {
                    if (!tag.startsWith(repositoryName)) {
                        int pos = tag.indexOf("/");
                        if (pos < 0) {
                            tag = server + "/" + repositoryName + "/" + tag;
                        } else {
                            tag = server + "/" + repositoryName + tag.substring(pos);
                        }
                    }
                } else {
                    if (!tag.startsWith(server)) {
                        int pos = tag.indexOf("/");
                        if (pos < 0) {
                            tag = server + "/" + repositoryName + "/" + tag;
                        } else {
                            tag = server + "/" + repositoryName + tag.substring(pos);
                        }
                    }
                }

                tags.computeIfAbsent(server, k -> new ArrayList<>()).add(tag);
            }
        }

        return tags;
    }

    private void push(String server, String imageName) throws PackagerProcessingException {
        Command cmd = createCommand("push")
            .arg("--quiet")
            .arg("--all-tags")
            .arg(imageName);

        context.getLogger().info(" - {}", imageName);
        context.getLogger().debug(RB.$("docker.push", imageName, server));
        context.getLogger().debug(String.join(" ", cmd.getArgs()));
        if (!context.isDryrun()) executeCommand(cmd);
    }

    private void logout(DockerConfiguration.Registry registry) throws PackagerProcessingException {
        if (registry.isExternalLogin()) return;

        Command cmd = createCommand("logout");
        if (isNotBlank(registry.getServer())) {
            cmd.arg(registry.getServerName());
        }

        context.getLogger().debug(RB.$("docker.logout"),
            registry.getServerName(),
            isNotBlank(registry.getServer()) ? " (" + registry.getServer() + ")" : "");
        if (!context.isDryrun()) executeCommand(cmd);
    }

    @Override
    protected void fillPackagerProperties(TemplateContext props, Distribution distribution) {
        props.set(KEY_DISTRIBUTION_JAVA_MAIN_CLASS, distribution.getJava().getMainClass());
        props.set(KEY_DISTRIBUTION_JAVA_MAIN_MODULE, distribution.getJava().getMainModule());
        fillDockerProperties(props, getPackager());
    }

    protected void fillDockerProperties(TemplateContext props, DockerConfiguration docker) {
        props.set(KEY_DOCKER_BASE_IMAGE,
            resolveTemplate(docker.getBaseImage(), props));

        List labels = new ArrayList<>();
        docker.getLabels().forEach((label, value) -> labels.add(passThrough("\"" + label + "\"=\"" +
            resolveTemplate(value, props) + "\"")));
        props.set(KEY_DOCKER_LABELS, labels);
        props.set(KEY_DOCKER_PRE_COMMANDS, docker.getPreCommands().stream()
            .map(c -> passThrough(resolveTemplate(c, props)))
            .collect(toList()));
        props.set(KEY_DOCKER_POST_COMMANDS, docker.getPostCommands().stream()
            .map(c -> passThrough(resolveTemplate(c, props)))
            .collect(toList()));
    }

    @Override
    protected void writeFile(Distribution distribution,
                             String content,
                             TemplateContext props,
                             Path outputDirectory,
                             String fileName) throws PackagerProcessingException {
        fileName = trimTplExtension(fileName);

        Path outputFile = "executable".equals(fileName) ?
            outputDirectory.resolve("assembly").resolve(distribution.getExecutable().getName()) :
            outputDirectory.resolve(fileName);

        writeFile(content, outputFile);
    }

    @Override
    protected void prepareWorkingCopy(TemplateContext props, Path directory, Distribution distribution) throws IOException {
        Path packageDirectory = props.get(KEY_DISTRIBUTION_PACKAGE_DIRECTORY);

        List activeSpecs = packager.getActiveSpecs();

        if (activeSpecs.isEmpty()) {
            for (String imageName : packager.getImageNames()) {
                copyDockerfiles(packageDirectory, resolveTemplate(imageName, props), directory, packager, false);
            }
        } else {
            // copy files that do not belong to specs
            prepareWorkingCopy(packageDirectory.resolve(ROOT), directory);

            for (DockerSpec spec : activeSpecs) {
                TemplateContext newProps = fillSpecProps(distribution, props, spec);
                for (String imageName : spec.getImageNames()) {
                    copyDockerfiles(packageDirectory.resolve(spec.getName()), resolveTemplate(imageName, newProps), directory, spec, true);
                }
            }
        }
    }

    private void copyDockerfiles(Path source, String imageName, Path directory, DockerConfiguration docker, boolean isSpec) throws IOException {
        Path destination = directory;

        String[] parts = imageName.split("/");
        parts = parts[parts.length - 1].split(":");
        if (isSpec) {
            destination = directory.resolve(parts[0]);
        }

        if (packager.getPackagerRepository().isVersionedSubfolders()) {
            destination = directory.resolve(parts[1]);
        }

        Path assembly = destination.resolve("assembly");
        FileUtils.deleteFiles(assembly);

        Files.createDirectories(destination);
        prepareWorkingCopy(source, destination,
            path -> !docker.isUseLocalArtifact() &&
                "assembly".equals(path.getFileName().toString()));
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy