Skip to content

Commit

Permalink
Enable buildx support in docker container image extension
Browse files Browse the repository at this point in the history
  • Loading branch information
edeandrea committed May 15, 2022
1 parent c63f1fc commit 4a2ffb4
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 44 deletions.
5 changes: 5 additions & 0 deletions docs/src/main/asciidoc/container-image.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.platform` configuration item 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.platform` property, the resulting images will not be loaded into `docker images`. If `quarkus.docker.platform` is omitted or if only a single platform is specified, it will then be loaded into `docker images`.

[#s2i]
=== S2I

Expand Down Expand Up @@ -190,6 +194,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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<List<String>> 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<String> 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<String> progress;

boolean useBuildx() {
return platform.filter(p -> !p.isEmpty()).isPresent() ||
output.isPresent() ||
progress.isPresent();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -162,8 +163,18 @@ 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();

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,
Expand All @@ -172,61 +183,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<String> 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<String> dockerArgs = new ArrayList<>(6 + dockerConfig.buildArgs.size());
dockerArgs.addAll(Arrays.asList("build", "-f", dockerfilePaths.getDockerfilePath().toAbsolutePath().toString()));
for (Map.Entry<String, String> entry : dockerConfig.buildArgs.entrySet()) {
dockerArgs.addAll(Arrays.asList("--build-arg", entry.getKey() + "=" + entry.getValue()));
}
for (Map.Entry<String, String> entry : containerImageConfig.labels.entrySet()) {
dockerArgs.addAll(Arrays.asList("--label", String.format("%s=%s", entry.getKey(), entry.getValue())));
}
if (dockerConfig.cacheFrom.isPresent()) {
List<String> 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]);
}
Expand Down Expand Up @@ -399,4 +458,4 @@ public Path getDockerExecutionPath() {
}
}

}
}

0 comments on commit 4a2ffb4

Please sign in to comment.