diff --git a/docs/src/main/asciidoc/container-image.adoc b/docs/src/main/asciidoc/container-image.adoc index 6b8b855cd9d69..66a887657d665 100644 --- a/docs/src/main/asciidoc/container-image.adoc +++ b/docs/src/main/asciidoc/container-image.adoc @@ -68,6 +68,10 @@ To use this feature, add the following extension to your project. :add-extension-extensions: container-image-docker include::includes/devtools/extension-add.adoc[] +The `quarkus-container-image-docker` extension is capable of https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images/[creating multi-platform (or multi-arch)] images using https://docs.docker.com/engine/reference/commandline/buildx_build/[`docker buildx build`]. See the `quarkus.docker.buildx.*` configuration items in the <<#DockerOptions,Docker Options>> section below. + +NOTE: `docker buildx build` ONLY supports https://docs.docker.com/engine/reference/commandline/buildx_build/#load[loading the result of a build] to `docker images` when building for a single platform. Therefore, if you specify more than one argument in the `quarkus.docker.buildx.platform` property, the resulting images will not be loaded into `docker images`. If `quarkus.docker.buildx.platform` is omitted or if only a single platform is specified, it will then be loaded into `docker images`. + [#s2i] === S2I @@ -138,7 +142,6 @@ To run tests on the resulting image, `quarkus.container-image.build=true` needs ./gradlew quarkusIntTest -Dquarkus.container-image.build=true ---- - == Pushing To push a container image for your project, `quarkus.container-image.push=true` needs to be set using any of the ways that Quarkus supports. @@ -190,6 +193,7 @@ In addition to the generic container image options, the `container-image-jib` al include::{generated-dir}/config/quarkus-container-image-jib.adoc[opts=optional, leveloffset=+1] +[#DockerOptions] === Docker Options In addition to the generic container image options, the `container-image-docker` also provides the following options: diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java index 86cd209ca2480..99d5420a51d92 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerConfig.java @@ -4,6 +4,8 @@ import java.util.Map; import java.util.Optional; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; @@ -51,4 +53,50 @@ public class DockerConfig { */ @ConfigItem(defaultValue = "docker") public String executableName; -} + + /** + * Configuration for Docker Buildx options + */ + @ConfigItem + @ConfigDocSection + public DockerBuildxConfig buildx; + + /** + * Configuration for Docker Buildx options. These are only relevant if using Docker Buildx + * (https://docs.docker.com/buildx/working-with-buildx/#build-multi-platform-images) to build multi-platform (or + * cross-platform) + * images. + * If any of these configurations are set, it will add {@code buildx} to the {@code executableName}. + */ + @ConfigGroup + public static class DockerBuildxConfig { + /** + * Which platform(s) to target during the build. See + * https://docs.docker.com/engine/reference/commandline/buildx_build/#platform + */ + @ConfigItem + public Optional> platform; + + /** + * Sets the export action for the build result. See + * https://docs.docker.com/engine/reference/commandline/buildx_build/#output. Note that any filesystem paths need to be + * absolute paths, + * not relative from where the command is executed from. + */ + @ConfigItem + public Optional output; + + /** + * Set type of progress output ({@code auto}, {@code plain}, {@code tty}). Use {@code plain} to show container output + * (default “{@code auto}”). See https://docs.docker.com/engine/reference/commandline/buildx_build/#progress + */ + @ConfigItem + public Optional progress; + + boolean useBuildx() { + return platform.filter(p -> !p.isEmpty()).isPresent() || + output.isPresent() || + progress.isPresent(); + } + } +} \ No newline at end of file diff --git a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java index d4552db4ddf1c..29bc53a8d1cd7 100644 --- a/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java +++ b/extensions/container-image/container-image-docker/deployment/src/main/java/io/quarkus/container/image/docker/deployment/DockerProcessor.java @@ -20,6 +20,7 @@ import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import java.util.stream.Stream; import org.jboss.logging.Logger; @@ -162,8 +163,34 @@ private String createContainerImage(ContainerImageConfig containerImageConfig, D OutputTargetBuildItem out, ImageIdReader reader, boolean forNative, boolean pushRequested, PackageConfig packageConfig) { + var useBuildx = dockerConfig.buildx.useBuildx(); + var pushImages = pushRequested || containerImageConfig.isPushExplicitlyEnabled(); + + // useBuildx: Whether or not any of the buildx parameters are set + // + // pushImages: Whether or not the user requested the built images to be pushed to a registry + // Pushing images is different based on if you're using buildx or not. + // If not using any of the buildx params (useBuildx == false), then the flow is as it was before: + // + // 1) Build the image (docker build) + // 2) Apply any tags (docker tag) + // 3) Push the image and all tags (docker push) + // + // If using any of the buildx options (useBuildx == true), the tagging & pushing happens + // as part of the 'docker buildx build' command via the added -t and --push params (see the getDockerArgs method). + // + // This is because when using buildx with more than one platform, the resulting images are not loaded into 'docker images'. + // Therefore, a docker tag or docker push will not work after a docker build. + DockerfilePaths dockerfilePaths = getDockerfilePaths(dockerConfig, forNative, packageConfig, out); - String[] dockerArgs = getDockerArgs(containerImageInfo.getImage(), dockerfilePaths, containerImageConfig, dockerConfig); + String[] dockerArgs = getDockerArgs(containerImageInfo.getImage(), dockerfilePaths, containerImageConfig, dockerConfig, + containerImageInfo, pushImages); + + if (useBuildx && pushImages) { + // Needed because buildx will push all the images in a single step + loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, dockerConfig); + } + log.infof("Executing the following command to build docker image: '%s %s'", dockerConfig.executableName, String.join(" ", dockerArgs)); boolean buildSuccessful = ExecUtil.exec(out.getOutputDirectory().toFile(), reader, dockerConfig.executableName, @@ -172,61 +199,109 @@ private String createContainerImage(ContainerImageConfig containerImageConfig, D throw dockerException(dockerArgs); } - log.infof("Built container image %s (%s)\n", containerImageInfo.getImage(), reader.getImageId()); - - if (!containerImageInfo.getAdditionalImageTags().isEmpty()) { - createAdditionalTags(containerImageInfo.getImage(), containerImageInfo.getAdditionalImageTags(), dockerConfig); - } - - if (pushRequested || containerImageConfig.isPushExplicitlyEnabled()) { - String registry = "docker.io"; - if (!containerImageInfo.getRegistry().isPresent()) { - log.info("No container image registry was set, so 'docker.io' will be used"); - } else { - registry = containerImageInfo.getRegistry().get(); - } - // Check if we need to login first - if (containerImageConfig.username.isPresent() && containerImageConfig.password.isPresent()) { - boolean loginSuccessful = ExecUtil.exec(dockerConfig.executableName, "login", registry, "-u", - containerImageConfig.username.get(), - "-p" + containerImageConfig.password.get()); - if (!loginSuccessful) { - throw dockerException(new String[] { "-u", containerImageConfig.username.get(), "-p", "********" }); - } + dockerConfig.buildx.platform + .filter(platform -> platform.size() > 1) + .ifPresentOrElse( + platform -> log.infof("Built container image %s (%s platform(s))\n", containerImageInfo.getImage(), + String.join(",", platform)), + () -> log.infof("Built container image %s (%s)\n", containerImageInfo.getImage(), reader.getImageId())); + + if (!useBuildx) { + // If we didn't use buildx, now we need to process any tags + if (!containerImageInfo.getAdditionalImageTags().isEmpty()) { + createAdditionalTags(containerImageInfo.getImage(), containerImageInfo.getAdditionalImageTags(), dockerConfig); } - List imagesToPush = new ArrayList<>(containerImageInfo.getAdditionalImageTags()); - imagesToPush.add(containerImageInfo.getImage()); - for (String imageToPush : imagesToPush) { - pushImage(imageToPush, dockerConfig); + if (pushImages) { + // If not using buildx, push the images + loginToRegistryIfNeeded(containerImageConfig, containerImageInfo, dockerConfig); + + Stream.concat(containerImageInfo.getAdditionalTags().stream(), Stream.of(containerImageInfo.getImage())) + .forEach(imageToPush -> pushImage(imageToPush, dockerConfig)); } } return containerImageInfo.getImage(); } + private void loginToRegistryIfNeeded(ContainerImageConfig containerImageConfig, + ContainerImageInfoBuildItem containerImageInfo, DockerConfig dockerConfig) { + var registry = containerImageInfo.getRegistry() + .orElseGet(() -> { + log.info("No container image registry was set, so 'docker.io' will be used"); + return "docker.io"; + }); + + // Check if we need to login first + if (containerImageConfig.username.isPresent() && containerImageConfig.password.isPresent()) { + boolean loginSuccessful = ExecUtil.exec(dockerConfig.executableName, "login", registry, "-u", + containerImageConfig.username.get(), + "-p" + containerImageConfig.password.get()); + if (!loginSuccessful) { + throw dockerException(new String[] { "-u", containerImageConfig.username.get(), "-p", "********" }); + } + } + } + private String[] getDockerArgs(String image, DockerfilePaths dockerfilePaths, ContainerImageConfig containerImageConfig, - DockerConfig dockerConfig) { + DockerConfig dockerConfig, ContainerImageInfoBuildItem containerImageInfo, boolean pushImages) { List dockerArgs = new ArrayList<>(6 + dockerConfig.buildArgs.size()); - dockerArgs.addAll(Arrays.asList("build", "-f", dockerfilePaths.getDockerfilePath().toAbsolutePath().toString())); - for (Map.Entry entry : dockerConfig.buildArgs.entrySet()) { - dockerArgs.addAll(Arrays.asList("--build-arg", entry.getKey() + "=" + entry.getValue())); - } - for (Map.Entry entry : containerImageConfig.labels.entrySet()) { - dockerArgs.addAll(Arrays.asList("--label", String.format("%s=%s", entry.getKey(), entry.getValue()))); - } - if (dockerConfig.cacheFrom.isPresent()) { - List cacheFrom = dockerConfig.cacheFrom.get(); - if (!cacheFrom.isEmpty()) { - dockerArgs.add("--cache-from"); - dockerArgs.add(String.join(",", cacheFrom)); + var useBuildx = dockerConfig.buildx.useBuildx(); + + if (useBuildx) { + // Check the executable. If not 'docker', then fail the build + if (!DOCKER.equals(dockerConfig.executableName)) { + throw new IllegalArgumentException( + String.format( + "The 'buildx' properties are specific to 'executable-name=docker' and can not be used with the '%s' executable name. Either remove the `buildx` properties or the `executable-name` property.", + dockerConfig.executableName)); } + + dockerArgs.add("buildx"); } - if (dockerConfig.network.isPresent()) { + + dockerArgs.addAll(Arrays.asList("build", "-f", dockerfilePaths.getDockerfilePath().toAbsolutePath().toString())); + dockerConfig.buildx.platform + .filter(platform -> !platform.isEmpty()) + .ifPresent(platform -> { + dockerArgs.add("--platform"); + dockerArgs.add(String.join(",", platform)); + + if (platform.size() == 1) { + // Buildx only supports loading the image to the docker system if there is only 1 image + dockerArgs.add("--load"); + } + }); + dockerConfig.buildx.progress.ifPresent(progress -> dockerArgs.addAll(List.of("--progress", progress))); + dockerConfig.buildx.output.ifPresent(output -> dockerArgs.addAll(List.of("--output", output))); + dockerConfig.buildArgs + .forEach((key, value) -> dockerArgs.addAll(Arrays.asList("--build-arg", String.format("%s=%s", key, value)))); + containerImageConfig.labels + .forEach((key, value) -> dockerArgs.addAll(Arrays.asList("--label", String.format("%s=%s", key, value)))); + dockerConfig.cacheFrom + .filter(cacheFrom -> !cacheFrom.isEmpty()) + .ifPresent(cacheFrom -> { + dockerArgs.add("--cache-from"); + dockerArgs.add(String.join(",", cacheFrom)); + }); + dockerConfig.network.ifPresent(network -> { dockerArgs.add("--network"); - dockerArgs.add(dockerConfig.network.get()); - } + dockerArgs.add(network); + }); dockerArgs.addAll(Arrays.asList("-t", image)); + + if (useBuildx) { + // When using buildx for multi-arch images, it wants to push in a single step + // 1) Create all the additional tags + containerImageInfo.getAdditionalImageTags() + .forEach(additionalImageTag -> dockerArgs.addAll(List.of("-t", additionalImageTag))); + + if (pushImages) { + // 2) Enable the --push flag + dockerArgs.add("--push"); + } + } + dockerArgs.add(dockerfilePaths.getDockerExecutionPath().toAbsolutePath().toString()); return dockerArgs.toArray(new String[0]); } @@ -399,4 +474,4 @@ public Path getDockerExecutionPath() { } } -} +} \ No newline at end of file