Skip to content

Commit

Permalink
pass extra args to docker container image extension
Browse files Browse the repository at this point in the history
Enable buildx support in docker container image extension
  • Loading branch information
edeandrea committed May 15, 2022
1 parent c63f1fc commit 6267670
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 42 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,71 +163,130 @@ 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,
dockerArgs);
if (!buildSuccessful) {
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

0 comments on commit 6267670

Please sign in to comment.