Add imagePlatform option for image building
An `imagePlatform` option for the Maven and Gradle image-building goal/task can be used to specify the os/architecture of any builder, run, and buildpack images that are pulled during image building. Closes gh-40944
This commit is contained in:
@@ -71,7 +71,7 @@ class BuildImageRegistryIntegrationTests extends AbstractArchiveIntegrationTests
|
||||
.contains("Pushed image '" + imageName + ":latest" + "'");
|
||||
ImageReference imageReference = ImageReference.of(imageName);
|
||||
DockerApi.ImageApi imageApi = new DockerApi().image();
|
||||
Image pulledImage = imageApi.pull(imageReference, UpdateListener.none());
|
||||
Image pulledImage = imageApi.pull(imageReference, null, UpdateListener.none());
|
||||
assertThat(pulledImage).isNotNull();
|
||||
imageApi.remove(imageReference, false);
|
||||
});
|
||||
|
||||
@@ -31,13 +31,14 @@ import org.junit.jupiter.api.condition.OS;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi;
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
|
||||
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.Image;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageName;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
|
||||
import org.springframework.boot.testsupport.container.DisabledIfDockerUnavailable;
|
||||
import org.springframework.boot.testsupport.junit.DisabledOnOs;
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -51,8 +52,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*/
|
||||
@ExtendWith(MavenBuildExtension.class)
|
||||
@DisabledIfDockerUnavailable
|
||||
@DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64",
|
||||
disabledReason = "The builder image has no ARM support")
|
||||
class BuildImageTests extends AbstractArchiveIntegrationTests {
|
||||
|
||||
@TestTemplate
|
||||
@@ -460,7 +459,6 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
|
||||
mavenBuild.project("dockerTest", "build-image-created-date")
|
||||
.goals("package")
|
||||
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
|
||||
.systemProperty("test-build-id", testBuildId)
|
||||
.execute((project) -> {
|
||||
assertThat(buildLog(project)).contains("Building image")
|
||||
.contains("docker.io/library/build-image-created-date:0.0.1.BUILD-SNAPSHOT")
|
||||
@@ -474,11 +472,9 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
|
||||
|
||||
@TestTemplate
|
||||
void whenBuildImageIsInvokedWithCurrentCreatedDate(MavenBuild mavenBuild) {
|
||||
String testBuildId = randomString();
|
||||
mavenBuild.project("dockerTest", "build-image-current-created-date")
|
||||
.goals("package")
|
||||
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
|
||||
.systemProperty("test-build-id", testBuildId)
|
||||
.execute((project) -> {
|
||||
assertThat(buildLog(project)).contains("Building image")
|
||||
.contains("docker.io/library/build-image-current-created-date:0.0.1.BUILD-SNAPSHOT")
|
||||
@@ -497,11 +493,9 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
|
||||
|
||||
@TestTemplate
|
||||
void whenBuildImageIsInvokedWithApplicationDirectory(MavenBuild mavenBuild) {
|
||||
String testBuildId = randomString();
|
||||
mavenBuild.project("dockerTest", "build-image-app-dir")
|
||||
.goals("package")
|
||||
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
|
||||
.systemProperty("test-build-id", testBuildId)
|
||||
.execute((project) -> {
|
||||
assertThat(buildLog(project)).contains("Building image")
|
||||
.contains("docker.io/library/build-image-app-dir:0.0.1.BUILD-SNAPSHOT")
|
||||
@@ -512,11 +506,9 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
|
||||
|
||||
@TestTemplate
|
||||
void whenBuildImageIsInvokedWithEmptySecurityOptions(MavenBuild mavenBuild) {
|
||||
String testBuildId = randomString();
|
||||
mavenBuild.project("dockerTest", "build-image-security-opts")
|
||||
.goals("package")
|
||||
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
|
||||
.systemProperty("test-build-id", testBuildId)
|
||||
.execute((project) -> {
|
||||
assertThat(buildLog(project)).contains("Building image")
|
||||
.contains("docker.io/library/build-image-security-opts:0.0.1.BUILD-SNAPSHOT")
|
||||
@@ -525,6 +517,49 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
@EnabledOnOs(value = { OS.LINUX, OS.MAC }, architectures = "aarch64",
|
||||
disabledReason = "Lifecycle will only run on ARM architecture")
|
||||
void whenBuildImageIsInvokedOnLinuxArmWithImagePlatformLinuxArm(MavenBuild mavenBuild) throws IOException {
|
||||
String builderImage = "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1";
|
||||
String runImage = "docker.io/paketobuildpacks/run-jammy-tiny:latest";
|
||||
String buildpackImage = "ghcr.io/spring-io/spring-boot-test-info:0.0.1";
|
||||
removeImages(builderImage, runImage, buildpackImage);
|
||||
mavenBuild.project("dockerTest", "build-image-platform-linux-arm").goals("package").execute((project) -> {
|
||||
File jar = new File(project, "target/build-image-platform-linux-arm-0.0.1.BUILD-SNAPSHOT.jar");
|
||||
assertThat(jar).isFile();
|
||||
assertThat(buildLog(project)).contains("Building image")
|
||||
.contains("docker.io/library/build-image-platform-linux-arm:0.0.1.BUILD-SNAPSHOT")
|
||||
.contains("Pulling builder image '" + builderImage + "' for platform 'linux/arm64'")
|
||||
.contains("Pulling run image '" + runImage + "' for platform 'linux/arm64'")
|
||||
.contains("Pulling buildpack image '" + buildpackImage + "' for platform 'linux/arm64'")
|
||||
.contains("---> Test Info buildpack building")
|
||||
.contains("---> Test Info buildpack done")
|
||||
.contains("Successfully built image");
|
||||
removeImage("docker.io/library/build-image-platform-linux-arm", "0.0.1.BUILD-SNAPSHOT");
|
||||
});
|
||||
removeImages(builderImage, runImage, buildpackImage);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
@EnabledOnOs(value = { OS.LINUX, OS.MAC }, architectures = "amd64",
|
||||
disabledReason = "The expected failure condition will not fail on ARM architectures")
|
||||
void failsWhenBuildImageIsInvokedOnLinuxAmdWithImagePlatformLinuxArm(MavenBuild mavenBuild) throws IOException {
|
||||
String builderImage = "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1";
|
||||
String runImage = "docker.io/paketobuildpacks/run-jammy-tiny:latest";
|
||||
String buildpackImage = "ghcr.io/spring-io/spring-boot-test-info:0.0.1";
|
||||
removeImages(buildpackImage, runImage, buildpackImage);
|
||||
mavenBuild.project("dockerTest", "build-image-platform-linux-arm")
|
||||
.goals("package")
|
||||
.executeAndFail((project) -> assertThat(buildLog(project)).contains("Building image")
|
||||
.contains("docker.io/library/build-image-platform-linux-arm:0.0.1.BUILD-SNAPSHOT")
|
||||
.contains("Pulling builder image '" + builderImage + "' for platform 'linux/arm64'")
|
||||
.contains("Pulling run image '" + runImage + "' for platform 'linux/arm64'")
|
||||
.contains("Pulling buildpack image '" + buildpackImage + "' for platform 'linux/arm64'")
|
||||
.contains("exec format error"));
|
||||
removeImages(builderImage, runImage, buildpackImage);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWhenBuildImageIsInvokedOnMultiModuleProjectWithBuildImageGoal(MavenBuild mavenBuild) {
|
||||
mavenBuild.project("dockerTest", "build-image-multi-module")
|
||||
@@ -582,6 +617,18 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
private void removeImages(String... names) throws IOException {
|
||||
ImageApi imageApi = new DockerApi().image();
|
||||
for (String name : names) {
|
||||
try {
|
||||
imageApi.remove(ImageReference.of(name), false);
|
||||
}
|
||||
catch (DockerEngineException ex) {
|
||||
// ignore image remove failures
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeImage(String name, String version) {
|
||||
ImageReference imageReference = ImageReference.of(ImageName.of(name), version);
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.boot.maven.it</groupId>
|
||||
<artifactId>build-image-platform-linux-arm</artifactId>
|
||||
<version>0.0.1.BUILD-SNAPSHOT</version>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>@java.version@</maven.compiler.source>
|
||||
<maven.compiler.target>@java.version@</maven.compiler.target>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>@project.groupId@</groupId>
|
||||
<artifactId>@project.artifactId@</artifactId>
|
||||
<version>@project.version@</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build-image-no-fork</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<image>
|
||||
<builder>ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1</builder>
|
||||
<runImage>paketobuildpacks/run-jammy-tiny</runImage>
|
||||
<buildpacks>
|
||||
<buildpack>ghcr.io/spring-io/spring-boot-test-info:0.0.1</buildpack>
|
||||
</buildpacks>
|
||||
<imagePlatform>linux/arm64</imagePlatform>
|
||||
</image>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.test;
|
||||
|
||||
public class SampleApplication {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("Launched");
|
||||
synchronized(args) {
|
||||
args.wait(); // Prevent exit
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -140,7 +140,14 @@ The following table summarizes the available parameters and their default values
|
||||
| `trustBuilder` +
|
||||
(`spring-boot.build-image.trustBuilder`)
|
||||
| Whether to treat the builder as https://buildpacks.io/docs/for-platform-operators/how-to/integrate-ci/pack/concepts/trusted_builders/#what-is-a-trusted-builder[trusted].
|
||||
| `true` if the builder is one of `paketobuildpacks/builder-jammy-tiny`, `paketobuildpacks/builder-jammy-base`, `paketobuildpacks/builder-jammy-full`, `paketobuildpacks/builder-jammy-buildpackless-tiny`, `paketobuildpacks/builder-jammy-buildpackless-base`, `paketobuildpacks/builder-jammy-buildpackless-full`, `gcr.io/buildpacks/builder`, `heroku/builder`; false otherwise.
|
||||
| `true` if the builder is one of `paketobuildpacks/builder-jammy-tiny`, `paketobuildpacks/builder-jammy-base`, `paketobuildpacks/builder-jammy-full`, `paketobuildpacks/builder-jammy-buildpackless-tiny`, `paketobuildpacks/builder-jammy-buildpackless-base`, `paketobuildpacks/builder-jammy-buildpackless-full`, `gcr.io/buildpacks/builder`, `heroku/builder`; `false` otherwise.
|
||||
|
||||
| `imagePlatform` +
|
||||
(`spring-boot.build-image.imagePlatform`)
|
||||
a|The platform (operating system and architecture) of any builder, run, and buildpack images that are pulled.
|
||||
Must be in the form of `OS[/architecture[/variant]]`, such as `linux/amd64`, `linux/arm64`, or `linux/arm/v5`.
|
||||
Refer to documentation of the builder being used to determine the image OS and architecture options available.
|
||||
| No default value, indicating that the platform of the host machine should be used.
|
||||
|
||||
| `runImage` +
|
||||
(`spring-boot.build-image.runImage`)
|
||||
|
||||
@@ -179,6 +179,14 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo {
|
||||
@Parameter(property = "spring-boot.build-image.applicationDirectory", readonly = true)
|
||||
String applicationDirectory;
|
||||
|
||||
/**
|
||||
* Alias for {@link Image#imagePlatform} to support configuration through command-line
|
||||
* property.
|
||||
* @since 3.4.0
|
||||
*/
|
||||
@Parameter(property = "spring-boot.build-image.imagePlatform", readonly = true)
|
||||
String imagePlatform;
|
||||
|
||||
/**
|
||||
* Docker configuration options.
|
||||
* @since 2.4.0
|
||||
@@ -299,6 +307,9 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo {
|
||||
if (image.applicationDirectory == null && this.applicationDirectory != null) {
|
||||
image.setApplicationDirectory(this.applicationDirectory);
|
||||
}
|
||||
if (image.imagePlatform == null && this.imagePlatform != null) {
|
||||
image.setImagePlatform(this.imagePlatform);
|
||||
}
|
||||
return customize(image.getBuildRequest(this.project.getArtifact(), content));
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,8 @@ public class Image {
|
||||
|
||||
List<String> securityOptions;
|
||||
|
||||
String imagePlatform;
|
||||
|
||||
/**
|
||||
* The name of the created image.
|
||||
* @return the image name
|
||||
@@ -219,6 +221,20 @@ public class Image {
|
||||
this.applicationDirectory = applicationDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the platform (os/architecture/variant) that will be used for all pulled
|
||||
* images. When {@code null}, the system will choose a platform based on the host
|
||||
* operating system and architecture.
|
||||
* @return the image platform
|
||||
*/
|
||||
public String getImagePlatform() {
|
||||
return this.imagePlatform;
|
||||
}
|
||||
|
||||
public void setImagePlatform(String imagePlatform) {
|
||||
this.imagePlatform = imagePlatform;
|
||||
}
|
||||
|
||||
BuildRequest getBuildRequest(Artifact artifact, Function<Owner, TarArchive> applicationContent) {
|
||||
return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent));
|
||||
}
|
||||
@@ -282,6 +298,9 @@ public class Image {
|
||||
if (this.securityOptions != null) {
|
||||
request = request.withSecurityOptions(this.securityOptions);
|
||||
}
|
||||
if (this.imagePlatform != null) {
|
||||
request = request.withImagePlatform(this.imagePlatform);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ import org.springframework.boot.buildpack.platform.build.BuildpackReference;
|
||||
import org.springframework.boot.buildpack.platform.build.Cache;
|
||||
import org.springframework.boot.buildpack.platform.build.PullPolicy;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.Binding;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||
import org.springframework.boot.buildpack.platform.io.Owner;
|
||||
import org.springframework.boot.buildpack.platform.io.TarArchive;
|
||||
@@ -80,6 +81,7 @@ class ImageTests {
|
||||
assertThat(request.getBuildpacks()).isEmpty();
|
||||
assertThat(request.getBindings()).isEmpty();
|
||||
assertThat(request.getNetwork()).isNull();
|
||||
assertThat(request.getImagePlatform()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -280,6 +282,14 @@ class ImageTests {
|
||||
assertThat(request.getSecurityOptions()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBuildRequestWhenHasImagePlatformUsesImagePlatform() {
|
||||
Image image = new Image();
|
||||
image.imagePlatform = "linux/arm64";
|
||||
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
|
||||
assertThat(request.getImagePlatform()).isEqualTo(ImagePlatform.of("linux/arm64"));
|
||||
}
|
||||
|
||||
private Artifact createArtifact() {
|
||||
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
|
||||
"jar", null, new DefaultArtifactHandler());
|
||||
|
||||
Reference in New Issue
Block a user