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
geoand authored and edeandrea committed May 15, 2022
1 parent 65175c1 commit e675000
Show file tree
Hide file tree
Showing 15 changed files with 249 additions and 115 deletions.
8 changes: 4 additions & 4 deletions bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@
<graal-sdk.version>22.1.0</graal-sdk.version>
<!-- Use previous graal version till https://github.com/oracle/graal/issues/4518 gets resolved -->
<graal-svm.version>22.0.0.2</graal-svm.version>
<gizmo.version>1.0.10.Final</gizmo.version>
<gizmo.version>1.0.11.Final</gizmo.version>
<jackson-bom.version>2.13.2.20220328</jackson-bom.version>
<commons-logging-jboss-logging.version>1.0.0.Final</commons-logging-jboss-logging.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
<commons-codec.version>1.15</commons-codec.version>
<classmate.version>1.5.1</classmate.version>
<hibernate-orm.version>5.6.8.Final</hibernate-orm.version> <!-- When updating, align bytebuddy.version to Hibernate needs as well (just below): -->
<hibernate-orm.version>5.6.9.Final</hibernate-orm.version> <!-- When updating, align bytebuddy.version to Hibernate needs as well (just below): -->
<bytebuddy.version>1.12.9</bytebuddy.version> <!-- Version controlled by Hibernate ORM's needs -->
<hibernate-reactive.version>1.1.4.Final</hibernate-reactive.version>
<hibernate-reactive.version>1.1.5.Beta2</hibernate-reactive.version>
<hibernate-validator.version>6.2.3.Final</hibernate-validator.version>
<hibernate-search.version>6.1.3.Final</hibernate-search.version>
<narayana.version>5.12.6.Final</narayana.version>
Expand Down Expand Up @@ -147,7 +147,7 @@
<azure-functions-java-library.version>1.4.2</azure-functions-java-library.version>
<kotlin.version>1.6.21</kotlin.version>
<kotlin.coroutine.version>1.6.1</kotlin.coroutine.version>
<kotlin-serialization.version>1.3.2</kotlin-serialization.version>
<kotlin-serialization.version>1.3.3</kotlin-serialization.version>
<kubernetes-client.version>5.12.2</kubernetes-client.version> <!-- Please check with Java Operator SDK team before updating -->
<dekorate.version>2.9.2</dekorate.version> <!-- Please check with Java Operator SDK team before updating -->
<maven-invoker.version>3.1.0</maven-invoker.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ private Project findDeploymentProject(Project project, QuarkusExtensionConfigura
}
if (deploymentProject == null) {
project.getLogger().warn("Unable to find deployment project with name: " + deploymentProjectName
+ ". You can configure the deployment project name by setting the 'deploymentArtifact' property in the plugin extension.");
+ ". You can configure the deployment project name by setting the 'deploymentModule' property in the plugin extension.");
}
}
return deploymentProject;
Expand Down
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
4 changes: 2 additions & 2 deletions docs/src/main/asciidoc/writing-extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ Please refer to https://github.com/quarkusio/quarkus/blob/{quarkus-version}/devt
You will need to apply the `io.quarkus.extension` plugin in the `runtime` module of your extension project.
The plugin includes the `extensionDescriptor` task that will generate `META-INF/quarkus-extension.properties` and `META-INF/quarkus-extension.yml` files.
The plugin also enables the `io.quarkus:quarkus-extension-processor` annotation processor in both `deployment` and `runtime` modules.
The name of the deployment module can be configured in the plugin by setting the `deploymentArtifact` property. The property is set to `deployment` by default:
The name of the deployment module can be configured in the plugin by setting the `deploymentModule` property. The property is set to `deployment` by default:

[source,groovy,subs=attributes+]
----
Expand All @@ -556,7 +556,7 @@ plugins {
}
quarkusExtension {
deploymentArtifact = 'deployment'
deploymentModule = 'deployment'
}
dependencies {
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
Loading

0 comments on commit e675000

Please sign in to comment.