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:
Scott Frederick
2024-07-30 12:54:14 -05:00
parent 67bae524b2
commit dfab18c965
45 changed files with 1373 additions and 275 deletions

View File

@@ -43,6 +43,7 @@ import org.junit.jupiter.api.condition.OS;
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.ImageReference;
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
@@ -64,8 +65,6 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
@GradleCompatibility(configurationCache = true)
@DisabledIfDockerUnavailable
@DisabledOnOs(os = { OS.LINUX, OS.MAC }, architecture = "aarch64",
disabledReason = "The builder image has no ARM support")
class BootBuildImageIntegrationTests {
GradleBuild gradleBuild;
@@ -407,6 +406,57 @@ class BootBuildImageIntegrationTests {
removeImages(projectName);
}
@TestTemplate
@EnabledOnOs(value = { OS.LINUX, OS.MAC }, architectures = "aarch64",
disabledReason = "Lifecycle will only run on ARM architecture")
void buildsImageOnLinuxArmWithImagePlatformLinuxArm() throws IOException {
writeMainClass();
writeLongNameResource();
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);
BuildResult result = this.gradleBuild.build("bootBuildImage");
String projectName = this.gradleBuild.getProjectDir().getName();
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput())
.contains("Pulling builder image '" + builderImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput())
.contains("Pulling builder image '" + builderImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput()).contains("Pulling run image '" + runImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput())
.contains("Pulling buildpack image '" + buildpackImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput()).contains("Running detector");
assertThat(result.getOutput()).contains("Running builder");
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImages(projectName, builderImage, runImage, buildpackImage);
}
@TestTemplate
@EnabledOnOs(value = { OS.LINUX, OS.MAC }, architectures = "amd64",
disabledReason = "The expected failure condition will not fail on ARM architectures")
void failsWhenBuildingOnLinuxAmdWithImagePlatformLinuxArm() throws IOException {
writeMainClass();
writeLongNameResource();
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);
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage");
String projectName = this.gradleBuild.getProjectDir().getName();
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED);
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput())
.contains("Pulling builder image '" + builderImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput()).contains("Pulling run image '" + runImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput())
.contains("Pulling buildpack image '" + buildpackImage + "' for platform 'linux/arm64'");
assertThat(result.getOutput()).contains("exec format error");
removeImages(builderImage, runImage, buildpackImage);
}
@TestTemplate
void failsWithInvalidCreatedDate() throws IOException {
writeMainClass();
@@ -589,7 +639,12 @@ class BootBuildImageIntegrationTests {
private void removeImages(String... names) throws IOException {
ImageApi imageApi = new DockerApi().image();
for (String name : names) {
imageApi.remove(ImageReference.of(name), false);
try {
imageApi.remove(ImageReference.of(name), false);
}
catch (DockerEngineException ex) {
// ignore image remove failures
}
}
}

View File

@@ -75,7 +75,7 @@ class BootBuildImageRegistryIntegrationTests {
.contains("Pushing image '" + imageName + ":latest" + "'")
.contains("Pushed image '" + imageName + ":latest" + "'");
ImageReference imageReference = ImageReference.of(imageName);
Image pulledImage = new DockerApi().image().pull(imageReference, UpdateListener.none());
Image pulledImage = new DockerApi().image().pull(imageReference, null, UpdateListener.none());
assertThat(pulledImage).isNotNull();
new DockerApi().image().remove(imageReference, false);
}

View File

@@ -0,0 +1,11 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootBuildImage {
builder = "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1"
runImage = "paketobuildpacks/run-jammy-tiny"
buildpacks = ["ghcr.io/spring-io/spring-boot-test-info:0.0.1"]
imagePlatform = "linux/arm64"
}

View File

@@ -0,0 +1,11 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootBuildImage {
builder = "ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1"
runImage = "paketobuildpacks/run-jammy-tiny"
buildpacks = ["ghcr.io/spring-io/spring-boot-test-info:0.0.1"]
imagePlatform = "linux/arm64"
}

View File

@@ -124,7 +124,14 @@ The following table summarizes the available properties and their default values
| `trustBuilder`
| `--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`
| `--image-platform`
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`
| `--runImage`

View File

@@ -326,6 +326,18 @@ public abstract class BootBuildImage extends DefaultTask {
@Option(option = "securityOptions", description = "Security options that will be applied to the builder container")
public abstract ListProperty<String> getSecurityOptions();
/**
* 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
*/
@Input
@Optional
@Option(option = "imagePlatform",
description = "The platform (os/architecture/variant) that will be used for all pulled images")
public abstract Property<String> getImagePlatform();
/**
* Returns the Docker configuration the builder will use.
* @return docker configuration.
@@ -377,6 +389,9 @@ public abstract class BootBuildImage extends DefaultTask {
request = customizeCreatedDate(request);
request = customizeApplicationDirectory(request);
request = customizeSecurityOptions(request);
if (getImagePlatform().isPresent()) {
request = request.withImagePlatform(getImagePlatform().get());
}
return request;
}

View File

@@ -31,6 +31,7 @@ import org.springframework.boot.buildpack.platform.build.BuildRequest;
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
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.gradle.junit.GradleProjectBuilder;
@@ -324,4 +325,15 @@ class BootBuildImageTests {
"label=role:ROLE");
}
@Test
void whenImagePlatformIsNotConfiguredThenRequestHasNoImagePlatform() {
assertThat(this.buildImage.createRequest().getImagePlatform()).isNull();
}
@Test
void whenImagePlatformIsConfiguredThenRequestHasImagePlatform() {
this.buildImage.getImagePlatform().set("linux/arm64/v1");
assertThat(this.buildImage.createRequest().getImagePlatform()).isEqualTo(ImagePlatform.of("linux/arm64/v1"));
}
}