Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable buildx support in docker container image extension #25589

Merged
merged 1 commit into from
May 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion 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.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

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
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,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();
edeandrea marked this conversation as resolved.
Show resolved Hide resolved

// 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,
Expand All @@ -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<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 +474,4 @@ public Path getDockerExecutionPath() {
}
}

}
}