From dfab18c965b0169924af7401f076d2a0b75057d1 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Tue, 30 Jul 2024 12:54:14 -0500 Subject: [PATCH] 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 --- .../docker/DockerApiIntegrationTests.java | 2 +- .../platform/build/AbstractBuildLog.java | 14 +- .../buildpack/platform/build/ApiVersions.java | 3 +- .../buildpack/platform/build/BuildLog.java | 5 +- .../platform/build/BuildRequest.java | 201 ++++++++++-------- .../buildpack/platform/build/Builder.java | 16 +- .../buildpack/platform/build/Lifecycle.java | 6 +- .../buildpack/platform/docker/DockerApi.java | 99 ++++++++- .../docker/transport/HttpClientTransport.java | 19 +- .../docker/transport/HttpTransport.java | 16 +- .../{build => docker/type}/ApiVersion.java | 28 +-- .../buildpack/platform/docker/type/Image.java | 23 ++ .../platform/docker/type/ImageArchive.java | 25 ++- .../platform/docker/type/ImagePlatform.java | 101 +++++++++ .../platform/build/ApiVersionsTests.java | 4 +- .../platform/build/BuildRequestTests.java | 8 + .../platform/build/BuilderTests.java | 158 ++++++++++---- .../platform/build/LifecycleTests.java | 101 +++++---- .../build/PrintStreamBuildLogTests.java | 9 +- .../platform/docker/DockerApiTests.java | 149 ++++++++++++- .../type}/ApiVersionTests.java | 17 +- .../docker/type/ImageArchiveTests.java | 2 +- .../docker/type/ImagePlatformTests.java | 70 ++++++ .../platform/docker/type/ImageTests.java | 14 +- .../platform/build/image-with-platform.json | 133 ++++++++++++ .../platform/build/print-stream-build-log.txt | 2 +- .../build/run-image-with-platform.json | 98 +++++++++ .../docker/type/image-archive-config.json | 12 +- .../docker/type/image-archive-manifest.json | 2 +- .../buildpack/platform/docker/type/image.json | 5 +- .../BootBuildImageIntegrationTests.java | 61 +++++- ...ootBuildImageRegistryIntegrationTests.java | 2 +- ...OnLinuxArmWithImagePlatformLinuxArm.gradle | 11 + ...OnLinuxAmdWithImagePlatformLinuxArm.gradle | 11 + .../pages/packaging-oci-image.adoc | 9 +- .../gradle/tasks/bundling/BootBuildImage.java | 15 ++ .../tasks/bundling/BootBuildImageTests.java | 12 ++ .../BuildImageRegistryIntegrationTests.java | 2 +- .../boot/maven/BuildImageTests.java | 67 +++++- .../build-image-platform-linux-arm/pom.xml | 39 ++++ .../main/java/org/test/SampleApplication.java | 28 +++ .../maven-plugin/pages/build-image.adoc | 9 +- .../boot/maven/BuildImageMojo.java | 11 + .../org/springframework/boot/maven/Image.java | 19 ++ .../boot/maven/ImageTests.java | 10 + 45 files changed, 1373 insertions(+), 275 deletions(-) rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/{build => docker/type}/ApiVersion.java (82%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/{build => docker/type}/ApiVersionTests.java (85%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatformTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-platform.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/run-image-with-platform.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageOnLinuxArmWithImagePlatformLinuxArm.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenBuildingOnLinuxAmdWithImagePlatformLinuxArm.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/projects/build-image-platform-linux-arm/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/projects/build-image-platform-linux-arm/src/main/java/org/test/SampleApplication.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/dockerTest/java/org/springframework/boot/buildpack/platform/docker/DockerApiIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/dockerTest/java/org/springframework/boot/buildpack/platform/docker/DockerApiIntegrationTests.java index 55f0cfda3b..4fb4e72c30 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/dockerTest/java/org/springframework/boot/buildpack/platform/docker/DockerApiIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/dockerTest/java/org/springframework/boot/buildpack/platform/docker/DockerApiIntegrationTests.java @@ -36,7 +36,7 @@ class DockerApiIntegrationTests { @Test void pullImage() throws IOException { this.docker.image() - .pull(ImageReference.of("gcr.io/paketo-buildpacks/builder:base"), + .pull(ImageReference.of("gcr.io/paketo-buildpacks/builder:base"), null, new TotalProgressPullListener(new TotalProgressBar("Pulling: "))); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java index 6c4ed5c71a..67ea22f85b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java @@ -22,6 +22,7 @@ import java.util.function.Consumer; import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; @@ -43,8 +44,17 @@ public abstract class AbstractBuildLog implements BuildLog { } @Override - public Consumer pullingImage(ImageReference imageReference, ImageType imageType) { - return getProgressConsumer(String.format(" > Pulling %s '%s'", imageType.getDescription(), imageReference)); + public Consumer pullingImage(ImageReference imageReference, ImagePlatform platform, + ImageType imageType) { + String message; + if (platform != null) { + message = String.format(" > Pulling %s '%s' for platform '%s'", imageType.getDescription(), imageReference, + platform); + } + else { + message = String.format(" > Pulling %s '%s'", imageType.getDescription(), imageReference); + } + return getProgressConsumer(message); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java index 2b1f474c06..44acbd14a5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersions.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -19,6 +19,7 @@ package org.springframework.boot.buildpack.platform.build; import java.util.Arrays; import java.util.stream.IntStream; +import org.springframework.boot.buildpack.platform.docker.type.ApiVersion; import org.springframework.util.StringUtils; /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java index c753db8bb8..43631983ce 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java @@ -22,6 +22,7 @@ import java.util.function.Consumer; import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; @@ -46,10 +47,12 @@ public interface BuildLog { /** * Log that an image is being pulled. * @param imageReference the image reference + * @param platform the platform of the image * @param imageType the image type * @return a consumer for progress update events */ - Consumer pullingImage(ImageReference imageReference, ImageType imageType); + Consumer pullingImage(ImageReference imageReference, ImagePlatform platform, + ImageType imageType); /** * Log that an image has been pulled. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index 0b0a4c5d32..a3df10e0da 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -27,6 +27,7 @@ import java.util.Map; import java.util.function.Function; 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; @@ -102,6 +103,8 @@ public class BuildRequest { private final List securityOptions; + private final ImagePlatform platform; + BuildRequest(ImageReference name, Function applicationContent) { Assert.notNull(name, "Name must not be null"); Assert.notNull(applicationContent, "ApplicationContent must not be null"); @@ -126,17 +129,19 @@ public class BuildRequest { this.createdDate = null; this.applicationDirectory = null; this.securityOptions = null; + this.platform = null; } BuildRequest(ImageReference name, Function applicationContent, ImageReference builder, - ImageReference runImage, Creator creator, Map env, boolean cleanCache, + Boolean trustBuilder, ImageReference runImage, Creator creator, Map env, boolean cleanCache, boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks, List bindings, String network, List tags, Cache buildWorkspace, Cache buildCache, Cache launchCache, Instant createdDate, String applicationDirectory, List securityOptions, - Boolean trustBuilder) { + ImagePlatform platform) { this.name = name; this.applicationContent = applicationContent; this.builder = builder; + this.trustBuilder = trustBuilder; this.runImage = runImage; this.creator = creator; this.env = env; @@ -154,7 +159,7 @@ public class BuildRequest { this.createdDate = createdDate; this.applicationDirectory = applicationDirectory; this.securityOptions = securityOptions; - this.trustBuilder = trustBuilder; + this.platform = platform; } /** @@ -164,10 +169,11 @@ public class BuildRequest { */ public BuildRequest withBuilder(ImageReference builder) { Assert.notNull(builder, "Builder must not be null"); - return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, - this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, - this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.trustBuilder, + this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, + this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, + this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, + this.platform); } /** @@ -178,10 +184,10 @@ public class BuildRequest { * @since 3.4.0 */ public BuildRequest withTrustBuilder(boolean trustBuilder) { - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -190,10 +196,11 @@ public class BuildRequest { * @return an updated build request */ public BuildRequest withRunImage(ImageReference runImageName) { - return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), - this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, - this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, + runImageName.inTaggedOrDigestForm(), this.creator, this.env, this.cleanCache, this.verboseLogging, + this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, + this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory, + this.securityOptions, this.platform); } /** @@ -203,10 +210,10 @@ public class BuildRequest { */ public BuildRequest withCreator(Creator creator) { Assert.notNull(creator, "Creator must not be null"); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, + this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, + this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -220,10 +227,11 @@ public class BuildRequest { Assert.hasText(value, "Value must not be empty"); Map env = new LinkedHashMap<>(this.env); env.put(name, value); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, - Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, - this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, - this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, + this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, + this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, + this.platform); } /** @@ -235,11 +243,11 @@ public class BuildRequest { Assert.notNull(env, "Env must not be null"); Map updatedEnv = new LinkedHashMap<>(this.env); updatedEnv.putAll(env); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, - Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy, - this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, - this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, - this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, + this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.network, this.tags, + this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory, + this.securityOptions, this.platform); } /** @@ -248,10 +256,10 @@ public class BuildRequest { * @return an updated build request */ public BuildRequest withCleanCache(boolean cleanCache) { - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, + this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, + this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -260,10 +268,10 @@ public class BuildRequest { * @return an updated build request */ public BuildRequest withVerboseLogging(boolean verboseLogging) { - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, + this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, + this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -272,10 +280,10 @@ public class BuildRequest { * @return an updated build request */ public BuildRequest withPullPolicy(PullPolicy pullPolicy) { - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, + this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, + this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -284,10 +292,10 @@ public class BuildRequest { * @return an updated build request */ public BuildRequest withPublish(boolean publish) { - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, + this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, + this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -309,10 +317,10 @@ public class BuildRequest { */ public BuildRequest withBuildpacks(List buildpacks) { Assert.notNull(buildpacks, "Buildpacks must not be null"); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, + this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, + this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -334,10 +342,10 @@ public class BuildRequest { */ public BuildRequest withBindings(List bindings) { Assert.notNull(bindings, "Bindings must not be null"); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -347,10 +355,10 @@ public class BuildRequest { * @since 2.6.0 */ public BuildRequest withNetwork(String network) { - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings, network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -370,10 +378,10 @@ public class BuildRequest { */ public BuildRequest withTags(List tags) { Assert.notNull(tags, "Tags must not be null"); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings, this.network, tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -384,10 +392,10 @@ public class BuildRequest { */ public BuildRequest withBuildWorkspace(Cache buildWorkspace) { Assert.notNull(buildWorkspace, "BuildWorkspace must not be null"); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings, this.network, this.tags, buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -397,10 +405,10 @@ public class BuildRequest { */ public BuildRequest withBuildCache(Cache buildCache) { Assert.notNull(buildCache, "BuildCache must not be null"); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, buildCache, + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -410,10 +418,10 @@ public class BuildRequest { */ public BuildRequest withLaunchCache(Cache launchCache) { Assert.notNull(launchCache, "LaunchCache must not be null"); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, launchCache, this.createdDate, - this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, this.platform); } /** @@ -423,10 +431,11 @@ public class BuildRequest { */ public BuildRequest withCreatedDate(String createdDate) { Assert.notNull(createdDate, "CreatedDate must not be null"); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, - parseCreatedDate(createdDate), this.applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, parseCreatedDate(createdDate), this.applicationDirectory, this.securityOptions, + this.platform); } private Instant parseCreatedDate(String createdDate) { @@ -448,10 +457,10 @@ public class BuildRequest { */ public BuildRequest withApplicationDirectory(String applicationDirectory) { Assert.notNull(applicationDirectory, "ApplicationDirectory must not be null"); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - applicationDirectory, this.securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, applicationDirectory, this.securityOptions, this.platform); } /** @@ -462,10 +471,25 @@ public class BuildRequest { */ public BuildRequest withSecurityOptions(List securityOptions) { Assert.notNull(securityOptions, "SecurityOption must not be null"); - return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, - this.network, this.tags, this.buildWorkspace, this.buildCache, this.launchCache, this.createdDate, - this.applicationDirectory, securityOptions, this.trustBuilder); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory, securityOptions, this.platform); + } + + /** + * Return a new {@link BuildRequest} with an updated image platform. + * @param platform the image platform + * @return an updated build request + * @since 3.4.0 + */ + public BuildRequest withImagePlatform(String platform) { + Assert.notNull(platform, "Platform must not be null"); + return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, + this.buildpacks, this.bindings, this.network, this.tags, this.buildWorkspace, this.buildCache, + this.launchCache, this.createdDate, this.applicationDirectory, this.securityOptions, + ImagePlatform.of(platform)); } /** @@ -648,6 +672,15 @@ public class BuildRequest { return this.securityOptions; } + /** + * Return the platform that should be used when pulling images. + * @return the platform or {@code null} + * @since 3.4.0 + */ + public ImagePlatform getImagePlatform() { + return this.platform; + } + /** * Factory method to create a new {@link BuildRequest} from a JAR file. * @param jarFile the source jar file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index 2549144662..8d90e84a84 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -29,6 +29,7 @@ import org.springframework.boot.buildpack.platform.docker.configuration.DockerCo import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; 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.ImagePlatform; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.IOBiConsumer; import org.springframework.boot.buildpack.platform.io.TarArchive; @@ -99,7 +100,8 @@ public class Builder { this.log.start(request); String domain = request.getBuilder().getDomain(); PullPolicy pullPolicy = request.getPullPolicy(); - ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy); + ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy, + request.getImagePlatform()); Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder()); BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage); request = withRunImageIfNeeded(request, builderMetadata); @@ -208,10 +210,13 @@ public class Builder { private final PullPolicy pullPolicy; - ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy) { + private ImagePlatform defaultPlatform; + + ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy, ImagePlatform platform) { this.domain = domain; this.authHeader = authHeader; this.pullPolicy = pullPolicy; + this.defaultPlatform = platform; } Image fetchImage(ImageType type, ImageReference reference) throws IOException { @@ -236,9 +241,12 @@ public class Builder { private Image pullImage(ImageReference reference, ImageType imageType) throws IOException { TotalProgressPullListener listener = new TotalProgressPullListener( - Builder.this.log.pullingImage(reference, imageType)); - Image image = Builder.this.docker.image().pull(reference, listener, this.authHeader); + Builder.this.log.pullingImage(reference, this.defaultPlatform, imageType)); + Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, this.authHeader); Builder.this.log.pulledImage(image, imageType); + if (this.defaultPlatform == null) { + this.defaultPlatform = ImagePlatform.from(image); + } return image; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index 98c58ae984..c2457da089 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -29,6 +29,7 @@ import com.sun.jna.Platform; import org.springframework.boot.buildpack.platform.docker.DockerApi; import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; +import org.springframework.boot.buildpack.platform.docker.type.ApiVersion; import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; import org.springframework.boot.buildpack.platform.docker.type.ContainerContent; @@ -363,7 +364,7 @@ class Lifecycle implements Closeable { private ContainerReference createContainer(ContainerConfig config, boolean requiresAppUpload) throws IOException { if (!requiresAppUpload || this.applicationVolumePopulated) { - return this.docker.container().create(config); + return this.docker.container().create(config, this.request.getImagePlatform()); } try { if (this.application.getBind() != null) { @@ -371,7 +372,8 @@ class Lifecycle implements Closeable { } TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner()); return this.docker.container() - .create(config, ContainerContent.of(applicationContent, this.applicationDirectory)); + .create(config, this.request.getImagePlatform(), + ContainerContent.of(applicationContent, this.applicationDirectory)); } finally { this.applicationVolumePopulated = true; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 6e7263af88..32a00f4775 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -28,17 +28,20 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.net.URIBuilder; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport; import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response; +import org.springframework.boot.buildpack.platform.docker.type.ApiVersion; import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; import org.springframework.boot.buildpack.platform.docker.type.ContainerContent; import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; +import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.boot.buildpack.platform.io.IOBiConsumer; @@ -61,7 +64,9 @@ public class DockerApi { private static final List FORCE_PARAMS = Collections.unmodifiableList(Arrays.asList("force", "1")); - static final String API_VERSION = "v1.24"; + static final ApiVersion MINIMUM_API_VERSION = ApiVersion.parse("1.24"); + + static final String API_VERSION_HEADER_NAME = "API-Version"; private final HttpTransport http; @@ -73,6 +78,10 @@ public class DockerApi { private final VolumeApi volume; + private final SystemApi system; + + private ApiVersion apiVersion = null; + /** * Create a new {@link DockerApi} instance. */ @@ -100,6 +109,7 @@ public class DockerApi { this.image = new ImageApi(); this.container = new ContainerApi(); this.volume = new VolumeApi(); + this.system = new SystemApi(); } private HttpTransport http() { @@ -116,7 +126,10 @@ public class DockerApi { private URI buildUrl(String path, Object... params) { try { - URIBuilder builder = new URIBuilder("/" + API_VERSION + path); + if (this.apiVersion == null) { + this.apiVersion = this.system.getApiVersion(); + } + URIBuilder builder = new URIBuilder("/v" + this.apiVersion + path); int param = 0; while (param < params.length) { builder.addParameter(Objects.toString(params[param++]), Objects.toString(params[param++])); @@ -128,6 +141,13 @@ public class DockerApi { } } + private void verifyApiVersionForPlatform() { + ApiVersion minimumPlatformApiVersion = ApiVersion.of(1, 41); + Assert.isTrue(this.apiVersion.supports(minimumPlatformApiVersion), + "Docker API version must be at least " + minimumPlatformApiVersion + + " to support the 'imagePlatform' option, but current API version is " + this.apiVersion); + } + /** * Return the Docker API for image operations. * @return the image API @@ -148,6 +168,10 @@ public class DockerApi { return this.volume; } + SystemApi system() { + return this.system; + } + /** * Docker API for image operations. */ @@ -159,27 +183,37 @@ public class DockerApi { /** * Pull an image from a registry. * @param reference the image reference to pull + * @param platform the platform (os/architecture/variant) of the image to pull * @param listener a pull listener to receive update events * @return the {@link ImageApi pulled image} instance * @throws IOException on IO error */ - public Image pull(ImageReference reference, UpdateListener listener) throws IOException { - return pull(reference, listener, null); + public Image pull(ImageReference reference, ImagePlatform platform, + UpdateListener listener) throws IOException { + return pull(reference, platform, listener, null); } /** * Pull an image from a registry. * @param reference the image reference to pull + * @param platform the platform (os/architecture/variant) of the image to pull * @param listener a pull listener to receive update events * @param registryAuth registry authentication credentials * @return the {@link ImageApi pulled image} instance * @throws IOException on IO error */ - public Image pull(ImageReference reference, UpdateListener listener, String registryAuth) - throws IOException { + public Image pull(ImageReference reference, ImagePlatform platform, + UpdateListener listener, String registryAuth) throws IOException { Assert.notNull(reference, "Reference must not be null"); Assert.notNull(listener, "Listener must not be null"); - URI createUri = buildUrl("/images/create", "fromImage", reference); + URI createUri; + if (platform != null) { + createUri = buildUrl("/images/create", "fromImage", reference, "platform", platform); + verifyApiVersionForPlatform(); + } + else { + createUri = buildUrl("/images/create", "fromImage", reference); + } DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener(); listener.onStart(); try { @@ -348,22 +382,32 @@ public class DockerApi { /** * Create a new container a {@link ContainerConfig}. * @param config the container config + * @param platform the platform (os/architecture/variant) of the image the + * container should be created from * @param contents additional contents to include * @return a {@link ContainerReference} for the newly created container * @throws IOException on IO error */ - public ContainerReference create(ContainerConfig config, ContainerContent... contents) throws IOException { + public ContainerReference create(ContainerConfig config, ImagePlatform platform, ContainerContent... contents) + throws IOException { Assert.notNull(config, "Config must not be null"); Assert.noNullElements(contents, "Contents must not contain null elements"); - ContainerReference containerReference = createContainer(config); + ContainerReference containerReference = createContainer(config, platform); for (ContainerContent content : contents) { uploadContainerContent(containerReference, content); } return containerReference; } - private ContainerReference createContainer(ContainerConfig config) throws IOException { - URI createUri = buildUrl("/containers/create"); + private ContainerReference createContainer(ContainerConfig config, ImagePlatform platform) throws IOException { + URI createUri; + if (platform != null) { + createUri = buildUrl("/containers/create", "platform", platform); + verifyApiVersionForPlatform(); + } + else { + createUri = buildUrl("/containers/create"); + } try (Response response = http().post(createUri, "application/json", config::writeTo)) { return ContainerReference .of(SharedObjectMapper.get().readTree(response.getContent()).at("/Id").asText()); @@ -460,6 +504,39 @@ public class DockerApi { } + /** + * Docker API for system operations. + */ + class SystemApi { + + SystemApi() { + } + + /** + * Get the API version supported by the Docker daemon. + * @return the Docker daemon API version + */ + ApiVersion getApiVersion() { + try { + URI uri = new URIBuilder("/_ping").build(); + try (Response response = http().head(uri)) { + Header apiVersionHeader = response.getHeader(API_VERSION_HEADER_NAME); + if (apiVersionHeader != null) { + return ApiVersion.parse(apiVersionHeader.getValue()); + } + } + catch (Exception ex) { + // fall through to return default value + } + return MINIMUM_API_VERSION; + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + + } + /** * {@link UpdateListener} used to capture the image digest. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java index c0387e1224..718912e2a3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -26,11 +26,13 @@ import java.net.URISyntaxException; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.classic.methods.HttpDelete; import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpHead; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.classic.methods.HttpPut; import org.apache.hc.client5.http.classic.methods.HttpUriRequest; import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.io.entity.AbstractHttpEntity; @@ -130,6 +132,16 @@ abstract class HttpClientTransport implements HttpTransport { return execute(new HttpDelete(uri)); } + /** + * Perform an HTTP HEAD operation. + * @param uri the destination URI + * @return the operation response + */ + @Override + public Response head(URI uri) { + return execute(new HttpHead(uri)); + } + private Response execute(HttpUriRequestBase request, String contentType, IOConsumer writer) { request.setEntity(new WritableHttpEntity(contentType, writer)); return execute(request); @@ -257,6 +269,11 @@ abstract class HttpClientTransport implements HttpTransport { return this.response.getEntity().getContent(); } + @Override + public Header getHeader(String name) { + return this.response.getFirstHeader(name); + } + @Override public void close() throws IOException { this.response.close(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java index c428155142..d18617fca5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -22,6 +22,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; +import org.apache.hc.core5.http.Header; + import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; @@ -89,6 +91,14 @@ public interface HttpTransport { */ Response delete(URI uri) throws IOException; + /** + * Perform an HTTP HEAD operation. + * @param uri the destination URI (excluding any host/port) + * @return the operation response + * @throws IOException on IO error + */ + Response head(URI uri) throws IOException; + /** * Create the most suitable {@link HttpTransport} based on the {@link DockerHost}. * @param dockerHost the Docker host information @@ -112,6 +122,10 @@ public interface HttpTransport { */ InputStream getContent() throws IOException; + default Header getHeader(String name) { + throw new UnsupportedOperationException(); + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersion.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersion.java similarity index 82% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersion.java rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersion.java index 6673768113..662680f4bd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ApiVersion.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersion.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.buildpack.platform.build; +package org.springframework.boot.buildpack.platform.docker.type; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,8 +26,9 @@ import org.springframework.util.Assert; * * @author Phillip Webb * @author Scott Frederick + * @since 3.4.0 */ -final class ApiVersion { +public final class ApiVersion { private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$"); @@ -56,27 +57,14 @@ final class ApiVersion { return this.minor; } - /** - * Assert that this API version supports the specified version. - * @param other the version to check against - * @see #supports(ApiVersion) - */ - void assertSupports(ApiVersion other) { - if (!supports(other)) { - throw new IllegalStateException( - "Detected platform API version '" + other + "' does not match supported version '" + this + "'"); - } - } - /** * Returns if this API version supports the given version. A {@code 0.x} matches only * the same version number. A 1.x or higher release matches when the versions have the * same major version and a minor that is equal or greater. * @param other the version to check against * @return if the specified API version is supported - * @see #assertSupports(ApiVersion) */ - boolean supports(ApiVersion other) { + public boolean supports(ApiVersion other) { if (equals(other)) { return true; } @@ -92,7 +80,7 @@ final class ApiVersion { * @return if any of the specified API versions are supported * @see #supports(ApiVersion) */ - boolean supportsAny(ApiVersion... others) { + public boolean supportsAny(ApiVersion... others) { for (ApiVersion other : others) { if (supports(other)) { return true; @@ -129,7 +117,7 @@ final class ApiVersion { * @return the corresponding {@link ApiVersion} * @throws IllegalArgumentException if the value could not be parsed */ - static ApiVersion parse(String value) { + public static ApiVersion parse(String value) { Assert.hasText(value, "Value must not be empty"); Matcher matcher = PATTERN.matcher(value); Assert.isTrue(matcher.matches(), () -> "Malformed version number '" + value + "'"); @@ -143,7 +131,7 @@ final class ApiVersion { } } - static ApiVersion of(int major, int minor) { + public static ApiVersion of(int major, int minor) { return new ApiVersion(major, minor); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java index dd224c8dab..55da2b2f8a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java @@ -31,6 +31,7 @@ import org.springframework.boot.buildpack.platform.json.MappedObject; * Image details as returned from {@code Docker inspect}. * * @author Phillip Webb + * @author Scott Frederick * @since 2.3.0 */ public class Image extends MappedObject { @@ -43,6 +44,10 @@ public class Image extends MappedObject { private final String os; + private final String architecture; + + private final String variant; + private final String created; Image(JsonNode node) { @@ -51,6 +56,8 @@ public class Image extends MappedObject { this.config = new ImageConfig(getNode().at("/Config")); this.layers = extractLayers(valueAt("/RootFS/Layers", String[].class)); this.os = valueAt("/Os", String.class); + this.architecture = valueAt("/Architecture", String.class); + this.variant = valueAt("/Variant", String.class); this.created = valueAt("/Created", String.class); } @@ -93,6 +100,22 @@ public class Image extends MappedObject { return (this.os != null) ? this.os : "linux"; } + /** + * Return the architecture of the image. + * @return the image architecture + */ + public String getArchitecture() { + return this.architecture; + } + + /** + * Return the variant of the image. + * @return the image variant + */ + public String getVariant() { + return this.variant; + } + /** * Return the created date of the image. * @return the image created date diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchive.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchive.java index ee1ab5cae7..8d8145ac41 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchive.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchive.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -76,17 +76,23 @@ public class ImageArchive implements TarArchive { private final String os; + private final String architecture; + + private final String variant; + private final List existingLayers; private final List newLayers; ImageArchive(ObjectMapper objectMapper, ImageConfig imageConfig, Instant createDate, ImageReference tag, String os, - List existingLayers, List newLayers) { + String architecture, String variant, List existingLayers, List newLayers) { this.objectMapper = objectMapper; this.imageConfig = imageConfig; this.createDate = createDate; this.tag = tag; this.os = os; + this.architecture = architecture; + this.variant = variant; this.existingLayers = existingLayers; this.newLayers = newLayers; } @@ -164,11 +170,13 @@ public class ImageArchive implements TarArchive { private ObjectNode createConfig(List writtenLayers) { ObjectNode config = this.objectMapper.createObjectNode(); - config.set("config", this.imageConfig.getNodeCopy()); - config.set("created", config.textNode(getCreatedDate())); - config.set("history", createHistory(writtenLayers)); - config.set("os", config.textNode(this.os)); - config.set("rootfs", createRootFs(writtenLayers)); + config.set("Config", this.imageConfig.getNodeCopy()); + config.set("Created", config.textNode(getCreatedDate())); + config.set("History", createHistory(writtenLayers)); + config.set("Os", config.textNode(this.os)); + config.set("Architecture", config.textNode(this.architecture)); + config.set("Variant", config.textNode(this.variant)); + config.set("RootFS", createRootFs(writtenLayers)); return config; } @@ -264,7 +272,8 @@ public class ImageArchive implements TarArchive { update.accept(this); Instant createDate = (this.createDate != null) ? this.createDate : WINDOWS_EPOCH_PLUS_SECOND; return new ImageArchive(SharedObjectMapper.get(), this.config, createDate, this.tag, this.image.getOs(), - this.image.getLayers(), Collections.unmodifiableList(this.newLayers)); + this.image.getArchitecture(), this.image.getVariant(), this.image.getLayers(), + Collections.unmodifiableList(this.newLayers)); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java new file mode 100644 index 0000000000..93d098135c --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java @@ -0,0 +1,101 @@ +/* + * 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.springframework.boot.buildpack.platform.docker.type; + +import java.util.Objects; + +import org.springframework.util.Assert; + +/** + * A platform specification for a Docker image. + * + * @author Scott Frederick + * @since 3.4.0 + */ +public class ImagePlatform { + + private final String os; + + private final String architecture; + + private final String variant; + + ImagePlatform(String os, String architecture, String variant) { + Assert.hasText(os, "OS must not be empty"); + this.os = os; + this.architecture = architecture; + this.variant = variant; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ImagePlatform that)) { + return false; + } + return Objects.equals(this.os, that.os) && Objects.equals(this.architecture, that.architecture) + && Objects.equals(this.variant, that.variant); + } + + @Override + public int hashCode() { + return Objects.hash(this.os, this.architecture, this.variant); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(this.os); + if (this.architecture != null) { + builder.append("/").append(this.architecture); + } + if (this.variant != null) { + builder.append("/").append(this.variant); + } + return builder.toString(); + } + + /** + * Create a new {@link ImagePlatform} from the given value in the form + * {@code os[/architecture[/variant]]}. + * @param value the value to parse + * @return an {@link ImagePlatform} instance + */ + public static ImagePlatform of(String value) { + Assert.hasText(value, "Value must not be empty"); + String[] split = value.split("/+"); + return switch (split.length) { + case 1 -> new ImagePlatform(split[0], null, null); + case 2 -> new ImagePlatform(split[0], split[1], null); + case 3 -> new ImagePlatform(split[0], split[1], split[2]); + default -> throw new IllegalArgumentException( + "ImagePlatform value '" + value + "' must be in the form of os[/architecture[/variant]]"); + }; + } + + /** + * Create a new {@link ImagePlatform} matching the platform information from the + * provided {@link Image}. + * @param image the image to get platform information from + * @return an {@link ImagePlatform} instance + */ + public static ImagePlatform from(Image image) { + return new ImagePlatform(image.getOs(), image.getArchitecture(), image.getVariant()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionsTests.java index 1028f49ea3..ae46e8c876 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -20,6 +20,8 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.Test; +import org.springframework.boot.buildpack.platform.docker.type.ApiVersion; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index d0eae13b3d..2d5490f728 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -39,6 +39,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ImageName; +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; @@ -392,6 +393,13 @@ class BuildRequestTests { assertThat(withAppDir.getSecurityOptions()).containsExactly("label=user:USER", "label=role:ROLE"); } + @Test + void withPlatformSetsPlatform() throws Exception { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withAppDir = request.withImagePlatform("linux/arm64"); + assertThat(withAppDir.getImagePlatform()).isEqualTo(ImagePlatform.of("linux/arm64")); + } + private void hasExpectedJarContent(TarArchive archive) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java index b6e9553dfd..78d2ea24c0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java @@ -36,6 +36,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ContainerReferenc import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; +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.TarArchive; @@ -86,9 +87,12 @@ class BuilderTests { DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image.json"); Image runImage = loadImage("run-image.json"); - given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest(); @@ -97,9 +101,10 @@ class BuilderTests { assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); then(docker.image()).should() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull()); + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()); then(docker.image()).should() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()); + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), isNull()); then(docker.image()).should().load(archive.capture(), any()); then(docker.image()).should().remove(archive.getValue().getTag(), true); then(docker.image()).shouldHaveNoMoreInteractions(); @@ -115,12 +120,12 @@ class BuilderTests { .withBuilderRegistryTokenAuthentication("builder token") .withPublishRegistryTokenAuthentication("publish token"); given(docker.image() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) .willAnswer(withPulledImage(builderImage)); given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), - eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); BuildRequest request = getTestRequest().withPublish(true); @@ -129,11 +134,11 @@ class BuilderTests { assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); then(docker.image()).should() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); then(docker.image()).should() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), - eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); then(docker.image()).should() .push(eq(request.getName()), any(), eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())); @@ -148,9 +153,12 @@ class BuilderTests { DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image-with-no-run-image-tag.json"); Image runImage = loadImage("run-image.json"); - given(docker.image().pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:latest")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:latest")), eq(ImagePlatform.from(builderImage)), + any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withBuilder(ImageReference.of("gcr.io/paketo-buildpacks/builder")); @@ -168,12 +176,13 @@ class BuilderTests { DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image-with-run-image-digest.json"); Image runImage = loadImage("run-image.json"); - given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); given(docker.image() .pull(eq(ImageReference .of("docker.io/cloudfoundry/run@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d")), - any(), isNull())) + eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest(); @@ -191,9 +200,12 @@ class BuilderTests { DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image-with-empty-stack.json"); Image runImage = loadImage("run-image.json"); - given(docker.image().pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withBuilder(ImageReference.of("gcr.io/paketo-buildpacks/builder")); @@ -211,9 +223,12 @@ class BuilderTests { DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image.json"); Image runImage = loadImage("run-image.json"); - given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("example.com/custom/run:latest")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("example.com/custom/run:latest")), eq(ImagePlatform.from(builderImage)), any(), + isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withRunImage(ImageReference.of("example.com/custom/run:latest")); @@ -231,9 +246,12 @@ class BuilderTests { DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image.json"); Image runImage = loadImage("run-image.json"); - given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), isNull())) .willAnswer(withPulledImage(runImage)); given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)))) .willReturn(builderImage); @@ -247,7 +265,7 @@ class BuilderTests { ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); then(docker.image()).should().load(archive.capture(), any()); then(docker.image()).should().remove(archive.getValue().getTag(), true); - then(docker.image()).should(never()).pull(any(), any()); + then(docker.image()).should(never()).pull(any(), any(), any()); then(docker.image()).should(times(2)).inspect(any()); } @@ -257,9 +275,12 @@ class BuilderTests { DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image.json"); Image runImage = loadImage("run-image.json"); - given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), isNull())) .willAnswer(withPulledImage(runImage)); given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)))) .willReturn(builderImage); @@ -273,7 +294,7 @@ class BuilderTests { ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); then(docker.image()).should().load(archive.capture(), any()); then(docker.image()).should().remove(archive.getValue().getTag(), true); - then(docker.image()).should(times(2)).pull(any(), any(), isNull()); + then(docker.image()).should(times(2)).pull(any(), any(), any(), isNull()); then(docker.image()).should(never()).inspect(any()); } @@ -283,9 +304,12 @@ class BuilderTests { DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image.json"); Image runImage = loadImage("run-image.json"); - given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), isNull())) .willAnswer(withPulledImage(runImage)); given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)))) .willThrow( @@ -304,7 +328,7 @@ class BuilderTests { then(docker.image()).should().load(archive.capture(), any()); then(docker.image()).should().remove(archive.getValue().getTag(), true); then(docker.image()).should(times(2)).inspect(any()); - then(docker.image()).should(times(2)).pull(any(), any(), isNull()); + then(docker.image()).should(times(2)).pull(any(), any(), any(), isNull()); } @Test @@ -313,9 +337,12 @@ class BuilderTests { DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image.json"); Image runImage = loadImage("run-image.json"); - given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withTags(ImageReference.of("my-application:1.2.3")); @@ -339,12 +366,12 @@ class BuilderTests { .withBuilderRegistryTokenAuthentication("builder token") .withPublishRegistryTokenAuthentication("publish token"); given(docker.image() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) .willAnswer(withPulledImage(builderImage)); given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), - eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); BuildRequest request = getTestRequest().withPublish(true).withTags(ImageReference.of("my-application:1.2.3")); @@ -354,11 +381,11 @@ class BuilderTests { assertThat(out.toString()).contains("Successfully created image tag 'docker.io/library/my-application:1.2.3'"); then(docker.image()).should() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); then(docker.image()).should() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), - eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); then(docker.image()).should() .push(eq(request.getName()), any(), eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())); @@ -372,15 +399,46 @@ class BuilderTests { then(docker.image()).shouldHaveNoMoreInteractions(); } + @Test + void buildInvokesBuilderWithPlatform() throws Exception { + TestPrintStream out = new TestPrintStream(); + ImagePlatform platform = ImagePlatform.of("linux/arm64/v1"); + DockerApi docker = mockDockerApi(platform); + Image builderImage = loadImage("image-with-platform.json"); + Image runImage = loadImage("run-image-with-platform.json"); + given(docker.image() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), eq(platform), any(), isNull())) + .willAnswer(withPulledImage(builderImage)); + given(docker.image() + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(platform), any(), isNull())) + .willAnswer(withPulledImage(runImage)); + Builder builder = new Builder(BuildLog.to(out), docker, null); + BuildRequest request = getTestRequest().withImagePlatform("linux/arm64/v1"); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + then(docker.image()).should() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), eq(platform), any(), isNull()); + then(docker.image()).should() + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(platform), any(), isNull()); + then(docker.image()).should().load(archive.capture(), any()); + then(docker.image()).should().remove(archive.getValue().getTag(), true); + then(docker.image()).shouldHaveNoMoreInteractions(); + } + @Test void buildWhenStackIdDoesNotMatchThrowsException() throws Exception { TestPrintStream out = new TestPrintStream(); DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image.json"); Image runImage = loadImage("run-image-with-bad-stack.json"); - given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest(); @@ -395,9 +453,12 @@ class BuilderTests { DockerApi docker = mockDockerApiLifecycleError(); Image builderImage = loadImage("image.json"); Image runImage = loadImage("run-image.json"); - given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), + any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest(); @@ -413,7 +474,7 @@ class BuilderTests { DockerConfiguration dockerConfiguration = new DockerConfiguration() .withBuilderRegistryTokenAuthentication("builder token"); given(docker.image() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) .willAnswer(withPulledImage(builderImage)); Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); @@ -431,7 +492,7 @@ class BuilderTests { DockerConfiguration dockerConfiguration = new DockerConfiguration() .withBuilderRegistryTokenAuthentication("builder token"); given(docker.image() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) .willAnswer(withPulledImage(builderImage)); Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); @@ -447,9 +508,10 @@ class BuilderTests { DockerApi docker = mockDockerApiLifecycleError(); Image builderImage = loadImage("image.json"); Image runImage = loadImage("run-image.json"); - given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), isNull())) + given(docker.image() + .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull())) + given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:example/buildpack@1.2.3"); @@ -460,9 +522,13 @@ class BuilderTests { } private DockerApi mockDockerApi() throws IOException { + return mockDockerApi(null); + } + + private DockerApi mockDockerApi(ImagePlatform platform) throws IOException { ContainerApi containerApi = mock(ContainerApi.class); ContainerReference reference = ContainerReference.of("container-ref"); - given(containerApi.create(any(), any())).willReturn(reference); + given(containerApi.create(any(), eq(platform), any())).willReturn(reference); given(containerApi.wait(eq(reference))).willReturn(ContainerStatus.of(0, null)); ImageApi imageApi = mock(ImageApi.class); VolumeApi volumeApi = mock(VolumeApi.class); @@ -476,7 +542,7 @@ class BuilderTests { private DockerApi mockDockerApiLifecycleError() throws IOException { ContainerApi containerApi = mock(ContainerApi.class); ContainerReference reference = ContainerReference.of("container-ref"); - given(containerApi.create(any(), any())).willReturn(reference); + given(containerApi.create(any(), isNull(), any())).willReturn(reference); given(containerApi.wait(eq(reference))).willReturn(ContainerStatus.of(9, null)); ImageApi imageApi = mock(ImageApi.class); VolumeApi volumeApi = mock(VolumeApi.class); @@ -499,7 +565,7 @@ class BuilderTests { private Answer withPulledImage(Image image) { return (invocation) -> { - TotalProgressPullListener listener = invocation.getArgument(1, TotalProgressPullListener.class); + TotalProgressPullListener listener = invocation.getArgument(2, TotalProgressPullListener.class); listener.onStart(); listener.onFinish(); return image; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 9769c1bf0b..47413d02f4 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -49,6 +49,7 @@ import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; import org.springframework.boot.buildpack.platform.docker.type.ContainerContent; import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; +import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.boot.buildpack.platform.io.IOConsumer; @@ -62,6 +63,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; @@ -92,8 +94,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeExecutesPhases(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); createLifecycle(trustBuilder).execute(); if (trustBuilder) { @@ -111,8 +113,8 @@ class LifecycleTests { @Test void executeWithBindingsExecutesPhases() throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(true).withBindings(Binding.of("/host/src/path:/container/dest/path:ro"), Binding.of("volume-name:/container/volume/path:rw")); @@ -123,8 +125,8 @@ class LifecycleTests { @Test void executeExecutesPhasesWithPlatformApi03() throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); createLifecycle(true, "builder-metadata-platform-api-0.3.json").execute(); assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-platform-api-0.3.json")); @@ -134,8 +136,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeOnlyUploadsContentOnce(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); createLifecycle(trustBuilder).execute(); assertThat(this.content).hasSize(1); @@ -144,8 +146,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeWhenAlreadyRunThrowsException(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); Lifecycle lifecycle = createLifecycle(trustBuilder); lifecycle.execute(); @@ -156,8 +158,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeWhenBuilderReturnsErrorThrowsException(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(9, null)); assertThatExceptionOfType(BuilderException.class).isThrownBy(() -> createLifecycle(trustBuilder).execute()) .withMessage( @@ -167,8 +169,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeWhenCleanCacheClearsCache(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(trustBuilder).withCleanCache(true); createLifecycle(request).execute(); @@ -188,8 +190,8 @@ class LifecycleTests { @Test void executeWhenPlatformApiNotSupportedThrowsException() throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); assertThatIllegalStateException() .isThrownBy(() -> createLifecycle(true, "builder-metadata-unsupported-api.json").execute()) @@ -198,8 +200,8 @@ class LifecycleTests { @Test void executeWhenMultiplePlatformApisNotSupportedThrowsException() throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); assertThatIllegalStateException() .isThrownBy(() -> createLifecycle(true, "builder-metadata-unsupported-apis.json").execute()) @@ -209,8 +211,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeWhenMultiplePlatformApisSupportedExecutesPhase(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); createLifecycle(trustBuilder, "builder-metadata-supported-apis.json").execute(); if (trustBuilder) { @@ -234,8 +236,8 @@ class LifecycleTests { @Test void executeWithNetworkExecutesPhases() throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(true).withNetwork("test"); createLifecycle(request).execute(); @@ -246,8 +248,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeWithCacheVolumeNamesExecutesPhases(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(trustBuilder).withBuildWorkspace(Cache.volume("work-volume")) .withBuildCache(Cache.volume("build-volume")) @@ -269,8 +271,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeWithCacheBindMountsExecutesPhases(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(trustBuilder).withBuildWorkspace(Cache.bind("/tmp/work")) .withBuildCache(Cache.bind("/tmp/build-cache")) @@ -292,8 +294,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeWithCreatedDateExecutesPhases(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(trustBuilder).withCreatedDate("2020-07-01T12:34:56Z"); createLifecycle(request).execute(); @@ -313,8 +315,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeWithApplicationDirectoryExecutesPhases(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(trustBuilder).withApplicationDirectory("/application"); createLifecycle(request).execute(); @@ -334,8 +336,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeWithSecurityOptionsExecutesPhases(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(trustBuilder) .withSecurityOptions(List.of("label=user:USER", "label=role:ROLE")); @@ -356,8 +358,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeWithDockerHostAndRemoteAddressExecutesPhases(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(trustBuilder); createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376"))) @@ -378,8 +380,8 @@ class LifecycleTests { @ParameterizedTest @BooleanValueSource void executeWithDockerHostAndLocalAddressExecutesPhases(boolean trustBuilder) throws Exception { - given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); - given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(trustBuilder); createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("/var/alt.sock"))) @@ -397,6 +399,29 @@ class LifecycleTests { assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } + @ParameterizedTest + @BooleanValueSource + void executeWithImagePlatformExecutesPhases(boolean trustBuilder) throws Exception { + given(this.docker.container().create(any(), eq(ImagePlatform.of("linux/arm64")))) + .willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), eq(ImagePlatform.of("linux/arm64")), any())) + .willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + BuildRequest request = getTestRequest(trustBuilder).withImagePlatform("linux/arm64"); + createLifecycle(request).execute(); + if (trustBuilder) { + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator.json")); + } + else { + assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer.json")); + assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json")); + assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer.json")); + assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json")); + assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter.json")); + } + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + private DockerApi mockDockerApi() { DockerApi docker = mock(DockerApi.class); ImageApi imageApi = mock(ImageApi.class); @@ -458,8 +483,8 @@ class LifecycleTests { ArrayNode command = getCommand(config); String name = command.get(0).asText().substring(1).replaceAll("/", "-"); this.configs.put(name, config); - if (invocation.getArguments().length > 1) { - this.content.put(name, invocation.getArgument(1, ContainerContent.class)); + if (invocation.getArguments().length > 2) { + this.content.put(name, invocation.getArgument(2, ContainerContent.class)); } return ContainerReference.of(name); }; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java index 1e25ed10a9..f1dbd3fd21 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; import org.springframework.boot.buildpack.platform.docker.type.Image; +import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.util.FileCopyUtils; @@ -51,6 +52,7 @@ class PrintStreamBuildLogTests { BuildRequest request = mock(BuildRequest.class); ImageReference name = ImageReference.of("my-app:latest"); ImageReference builderImageReference = ImageReference.of("cnb/builder"); + ImagePlatform platform = ImagePlatform.of("linux/arm64/v1"); Image builderImage = mock(Image.class); given(builderImage.getDigests()).willReturn(Collections.singletonList("00000001")); ImageReference runImageReference = ImageReference.of("cnb/runner"); @@ -60,11 +62,12 @@ class PrintStreamBuildLogTests { ImageReference tag = ImageReference.of("my-app:1.0"); given(request.getTags()).willReturn(Collections.singletonList(tag)); log.start(request); - Consumer pullBuildImageConsumer = log.pullingImage(builderImageReference, + Consumer pullBuildImageConsumer = log.pullingImage(builderImageReference, null, ImageType.BUILDER); pullBuildImageConsumer.accept(new TotalProgressEvent(100)); log.pulledImage(builderImage, ImageType.BUILDER); - Consumer pullRunImageConsumer = log.pullingImage(runImageReference, ImageType.RUNNER); + Consumer pullRunImageConsumer = log.pullingImage(runImageReference, platform, + ImageType.RUNNER); pullRunImageConsumer.accept(new TotalProgressEvent(100)); log.pulledImage(runImage, ImageType.RUNNER); log.executingLifecycle(request, LifecycleVersion.parse("0.5"), Cache.volume(VolumeName.of("pack-abc.cache"))); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index 4f1dce08ac..619f23ea2c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -18,15 +18,19 @@ package org.springframework.boot.buildpack.platform.docker; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -39,15 +43,18 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi; +import org.springframework.boot.buildpack.platform.docker.DockerApi.SystemApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi; import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport; import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response; +import org.springframework.boot.buildpack.platform.docker.type.ApiVersion; import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; import org.springframework.boot.buildpack.platform.docker.type.ContainerContent; import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageArchive; +import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.boot.buildpack.platform.io.Content; @@ -80,12 +87,18 @@ import static org.mockito.Mockito.times; @ExtendWith(MockitoExtension.class) class DockerApiTests { - private static final String API_URL = "/" + DockerApi.API_VERSION; + private static final String API_URL = "/v" + DockerApi.MINIMUM_API_VERSION; + + public static final String PING_URL = "/_ping"; private static final String IMAGES_URL = API_URL + "/images"; + private static final String IMAGES_1_41_URL = "/v" + ApiVersion.of(1, 41) + "/images"; + private static final String CONTAINERS_URL = API_URL + "/containers"; + private static final String CONTAINERS_1_41_URL = "/v" + ApiVersion.of(1, 41) + "/containers"; + private static final String VOLUMES_URL = API_URL + "/volumes"; @Mock @@ -124,6 +137,29 @@ class DockerApiTests { }; } + private Response responseWithHeaders(Header... headers) { + return new Response() { + + @Override + public InputStream getContent() { + return null; + } + + @Override + public Header getHeader(String name) { + return Arrays.stream(headers) + .filter((header) -> header.getName().equals(name)) + .findFirst() + .orElse(null); + } + + @Override + public void close() { + } + + }; + } + @Test void createDockerApi() { DockerApi api = new DockerApi(); @@ -154,13 +190,14 @@ class DockerApiTests { @Test void pullWhenReferenceIsNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(null, this.pullListener)) + assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(null, null, this.pullListener)) .withMessage("Reference must not be null"); } @Test void pullWhenListenerIsNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(ImageReference.of("ubuntu"), null)) + assertThatIllegalArgumentException() + .isThrownBy(() -> this.api.pull(ImageReference.of("ubuntu"), null, null)) .withMessage("Listener must not be null"); } @@ -171,7 +208,7 @@ class DockerApiTests { URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json"); given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json")); given(http().get(imageUri)).willReturn(responseOf("type/image.json")); - Image image = this.api.pull(reference, this.pullListener); + Image image = this.api.pull(reference, null, this.pullListener); assertThat(image.getLayers()).hasSize(46); InOrder ordered = inOrder(this.pullListener); ordered.verify(this.pullListener).onStart(); @@ -186,7 +223,7 @@ class DockerApiTests { URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json"); given(http().post(eq(createUri), eq("auth token"))).willReturn(responseOf("pull-stream.json")); given(http().get(imageUri)).willReturn(responseOf("type/image.json")); - Image image = this.api.pull(reference, this.pullListener, "auth token"); + Image image = this.api.pull(reference, null, this.pullListener, "auth token"); assertThat(image.getLayers()).hasSize(46); InOrder ordered = inOrder(this.pullListener); ordered.verify(this.pullListener).onStart(); @@ -194,6 +231,36 @@ class DockerApiTests { ordered.verify(this.pullListener).onFinish(); } + @Test + void pullWithPlatformPullsImageAndProducesEvents() throws Exception { + ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); + ImagePlatform platform = ImagePlatform.of("linux/arm64/v1"); + URI createUri = new URI(IMAGES_1_41_URL + + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1"); + URI imageUri = new URI(IMAGES_1_41_URL + "/gcr.io/paketo-buildpacks/builder:base/json"); + given(http().head(eq(new URI(PING_URL)))) + .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41"))); + given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json")); + given(http().get(imageUri)).willReturn(responseOf("type/image.json")); + Image image = this.api.pull(reference, platform, this.pullListener); + assertThat(image.getLayers()).hasSize(46); + InOrder ordered = inOrder(this.pullListener); + ordered.verify(this.pullListener).onStart(); + ordered.verify(this.pullListener, times(595)).onUpdate(any()); + ordered.verify(this.pullListener).onFinish(); + } + + @Test + void pullWithPlatformAndInsufficientApiVersionThrowsException() throws Exception { + ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); + ImagePlatform platform = ImagePlatform.of("linux/arm64/v1"); + given(http().head(eq(new URI(PING_URL)))).willReturn(responseWithHeaders( + new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, DockerApi.MINIMUM_API_VERSION))); + assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(reference, platform, this.pullListener)) + .withMessageContaining("must be at least 1.41") + .withMessageContaining("current API version is 1.24"); + } + @Test void pushWhenReferenceIsNullThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> this.api.push(null, this.pushListener, null)) @@ -460,7 +527,7 @@ class DockerApiTests { @Test void createWhenConfigIsNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(null)) + assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(null, null)) .withMessage("Config must not be null"); } @@ -471,7 +538,7 @@ class DockerApiTests { URI createUri = new URI(CONTAINERS_URL + "/create"); given(http().post(eq(createUri), eq("application/json"), any())) .willReturn(responseOf("create-container-response.json")); - ContainerReference containerReference = this.api.create(config); + ContainerReference containerReference = this.api.create(config, null); assertThat(containerReference).hasToString("e90e34656806"); then(http()).should().post(any(), any(), this.writer.capture()); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -493,7 +560,7 @@ class DockerApiTests { .willReturn(responseOf("create-container-response.json")); URI uploadUri = new URI(CONTAINERS_URL + "/e90e34656806/archive?path=%2F"); given(http().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse()); - ContainerReference containerReference = this.api.create(config, content); + ContainerReference containerReference = this.api.create(config, null, content); assertThat(containerReference).hasToString("e90e34656806"); then(http()).should().post(any(), any(), this.writer.capture()); ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -504,6 +571,34 @@ class DockerApiTests { assertThat(out.toByteArray()).hasSizeGreaterThan(2000); } + @Test + void createWithPlatformCreatesContainer() throws Exception { + ImageReference imageReference = ImageReference.of("ubuntu:bionic"); + ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash")); + ImagePlatform platform = ImagePlatform.of("linux/arm64/v1"); + given(http().head(eq(new URI(PING_URL)))) + .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41"))); + URI createUri = new URI(CONTAINERS_1_41_URL + "/create?platform=linux%2Farm64%2Fv1"); + given(http().post(eq(createUri), eq("application/json"), any())) + .willReturn(responseOf("create-container-response.json")); + ContainerReference containerReference = this.api.create(config, platform); + assertThat(containerReference).hasToString("e90e34656806"); + then(http()).should().post(any(), any(), this.writer.capture()); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + this.writer.getValue().accept(out); + assertThat(out.toByteArray()).hasSize(config.toString().length()); + } + + @Test + void createWithPlatformAndInsufficientApiVersionThrowsException() throws Exception { + ImageReference imageReference = ImageReference.of("ubuntu:bionic"); + ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash")); + ImagePlatform platform = ImagePlatform.of("linux/arm64/v1"); + assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(config, platform)) + .withMessageContaining("must be at least 1.41") + .withMessageContaining("current API version is 1.24"); + } + @Test void startWhenReferenceIsNullThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> this.api.start(null)) @@ -621,4 +716,42 @@ class DockerApiTests { } + @Nested + class SystemDockerApiTests { + + private SystemApi api; + + @BeforeEach + void setup() { + this.api = DockerApiTests.this.dockerApi.system(); + } + + @Test + void getApiVersionWithVersionHeaderReturnsVersion() throws Exception { + given(http().head(eq(new URI(PING_URL)))) + .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.44"))); + assertThat(this.api.getApiVersion()).isEqualTo(ApiVersion.of(1, 44)); + } + + @Test + void getApiVersionWithEmptyVersionHeaderReturnsDefaultVersion() throws Exception { + given(http().head(eq(new URI(PING_URL)))) + .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, ""))); + assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION); + } + + @Test + void getApiVersionWithNoVersionHeaderReturnsDefaultVersion() throws Exception { + given(http().head(eq(new URI(PING_URL)))).willReturn(emptyResponse()); + assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION); + } + + @Test + void getApiVersionWithExceptionReturnsDefaultVersion() throws Exception { + given(http().head(eq(new URI(PING_URL)))).willThrow(new IOException("simulated error")); + assertThat(this.api.getApiVersion()).isEqualTo(DockerApi.MINIMUM_API_VERSION); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersionTests.java similarity index 85% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersionTests.java index 1afb601232..d06d315eeb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/ApiVersionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ApiVersionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.buildpack.platform.build; +package org.springframework.boot.buildpack.platform.docker.type; import java.util.Arrays; @@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link ApiVersion}. @@ -57,18 +56,6 @@ class ApiVersionTests { assertThat(version.getMinor()).isEqualTo(2); } - @Test - void assertSupportsWhenSupports() { - ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.0")); - } - - @Test - void assertSupportsWhenDoesNotSupportThrowsException() { - assertThatIllegalStateException() - .isThrownBy(() -> ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.3"))) - .withMessage("Detected platform API version '1.3' does not match supported version '1.2'"); - } - @Test void supportsWhenSame() { assertThat(supports("0.0", "0.0")).isTrue(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveTests.java index 123f49bbcd..3d303df98d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageArchiveTests.java @@ -78,7 +78,7 @@ class ImageArchiveTests extends AbstractJsonTests { } private void assertExpectedConfig(TarArchiveEntry entry, byte[] content) throws Exception { - assertThat(entry.getName()).isEqualTo("682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json"); + assertThat(entry.getName()).isEqualTo("416c76dc7f691f91e80516ff039e056f32f996b59af4b1cb8114e6ae8171a374.json"); String actualJson = new String(content, StandardCharsets.UTF_8); String expectedJson = StreamUtils.copyToString(getContent("image-archive-config.json"), StandardCharsets.UTF_8); JSONAssert.assertEquals(expectedJson, actualJson, false); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatformTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatformTests.java new file mode 100644 index 0000000000..46da80ad23 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatformTests.java @@ -0,0 +1,70 @@ +/* + * 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.springframework.boot.buildpack.platform.docker.type; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +class ImagePlatformTests extends AbstractJsonTests { + + @Test + void ofWithOsParses() { + ImagePlatform platform = ImagePlatform.of("linux"); + assertThat(platform.toString()).isEqualTo("linux"); + } + + @Test + void ofWithOsAndArchitectureParses() { + ImagePlatform platform = ImagePlatform.of("linux/amd64"); + assertThat(platform.toString()).isEqualTo("linux/amd64"); + } + + @Test + void ofWithOsAndArchitectureAndVariantParses() { + ImagePlatform platform = ImagePlatform.of("linux/amd64/v1"); + assertThat(platform.toString()).isEqualTo("linux/amd64/v1"); + } + + @Test + void ofWithEmptyValueFails() { + assertThatIllegalArgumentException().isThrownBy(() -> ImagePlatform.of("")) + .withMessageContaining("Value must not be empty"); + } + + @Test + void ofWithTooManySegmentsFails() { + assertThatIllegalArgumentException().isThrownBy(() -> ImagePlatform.of("linux/amd64/v1/extra")) + .withMessageContaining("value 'linux/amd64/v1/extra'"); + } + + @Test + void fromImageMatchesImage() throws IOException { + ImagePlatform platform = ImagePlatform.from(getImage()); + assertThat(platform.toString()).isEqualTo("linux/amd64/v1"); + } + + private Image getImage() throws IOException { + return Image.of(getContent("image.json")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageTests.java index 851a9c781a..2bcea073ec 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/type/ImageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * 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. @@ -67,6 +67,18 @@ class ImageTests extends AbstractJsonTests { assertThat(image.getOs()).isEqualTo("linux"); } + @Test + void getArchitectureReturnsArchitecture() throws Exception { + Image image = getImage(); + assertThat(image.getArchitecture()).isEqualTo("amd64"); + } + + @Test + void getVariantReturnsVariant() throws Exception { + Image image = getImage(); + assertThat(image.getVariant()).isEqualTo("v1"); + } + @Test void getCreatedReturnsDate() throws Exception { Image image = getImage(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-platform.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-platform.json new file mode 100644 index 0000000000..715d3ea4b7 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/image-with-platform.json @@ -0,0 +1,133 @@ +{ + "Id": "sha256:44cc64492fb6a6d78d3e6d087f380ae6e479aa1b2c79823b32cdacfcc2f3d715", + "RepoTags": [ + "paketo-buildpacks/cnb:base", + "paketo-buildpacks/builder:base-platform-api-0.2" + ], + "RepoDigests": [ + "paketo-buidpacks/cnb@sha256:5b03a853e636b78c44e475bbc514e2b7b140cc41cca8ab907e9753431ae8c0b0" + ], + "Parent": "", + "Comment": "", + "Created": "1980-01-01T00:00:01Z", + "Container": "", + "ContainerConfig": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": null, + "Cmd": null, + "Image": "", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "DockerVersion": "", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "1000:1000", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "CNB_USER_ID=1000", + "CNB_GROUP_ID=1000", + "CNB_STACK_ID=io.buildpacks.stacks.bionic" + ], + "Cmd": [ + "/bin/bash" + ], + "ArgsEscaped": true, + "Image": "sha256:2d153261a5e359c632a17377cfb5d1986c27b96c8b6e95334bf80f1029dbd4bb", + "Volumes": null, + "WorkingDir": "/layers", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.builder.metadata": "{\"description\":\"Ubuntu bionic base image with buildpacks for Java, NodeJS and Golang\",\"buildpacks\":[{\"id\":\"paketo-buildpacks/dotnet-core\",\"version\":\"0.0.9\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core\"},{\"id\":\"paketo-buildpacks/dotnet-core-runtime\",\"version\":\"0.0.201\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-runtime\"},{\"id\":\"paketo-buildpacks/dotnet-core-sdk\",\"version\":\"0.0.196\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-sdk\"},{\"id\":\"paketo-buildpacks/dotnet-execute\",\"version\":\"0.0.180\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-execute\"},{\"id\":\"paketo-buildpacks/dotnet-publish\",\"version\":\"0.0.121\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-publish\"},{\"id\":\"paketo-buildpacks/dotnet-core-aspnet\",\"version\":\"0.0.196\",\"homepage\":\"https://github.com/paketo-buildpacks/dotnet-core-aspnet\"},{\"id\":\"paketo-buildpacks/java-native-image\",\"version\":\"4.7.0\",\"homepage\":\"https://github.com/paketo-buildpacks/java-native-image\"},{\"id\":\"paketo-buildpacks/spring-boot\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot\"},{\"id\":\"paketo-buildpacks/executable-jar\",\"version\":\"3.1.3\",\"homepage\":\"https://github.com/paketo-buildpacks/executable-jar\"},{\"id\":\"paketo-buildpacks/graalvm\",\"version\":\"4.1.0\",\"homepage\":\"https://github.com/paketo-buildpacks/graalvm\"},{\"id\":\"paketo-buildpacks/gradle\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/gradle\"},{\"id\":\"paketo-buildpacks/leiningen\",\"version\":\"1.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/leiningen\"},{\"id\":\"paketo-buildpacks/sbt\",\"version\":\"3.6.0\",\"homepage\":\"https://github.com/paketo-buildpacks/sbt\"},{\"id\":\"paketo-buildpacks/spring-boot-native-image\",\"version\":\"2.0.1\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot-native-image\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"2.1.2\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"2.0.7\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/maven\",\"version\":\"3.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/maven\"},{\"id\":\"paketo-buildpacks/java\",\"version\":\"4.10.0\",\"homepage\":\"https://github.com/paketo-buildpacks/java\"},{\"id\":\"paketo-buildpacks/ca-certificates\",\"version\":\"1.0.1\",\"homepage\":\"https://github.com/paketo-buildpacks/ca-certificates\"},{\"id\":\"paketo-buildpacks/environment-variables\",\"version\":\"2.1.2\",\"homepage\":\"https://github.com/paketo-buildpacks/environment-variables\"},{\"id\":\"paketo-buildpacks/executable-jar\",\"version\":\"3.1.3\",\"homepage\":\"https://github.com/paketo-buildpacks/executable-jar\"},{\"id\":\"paketo-buildpacks/procfile\",\"version\":\"3.0.0\",\"homepage\":\"https://github.com/paketo-buildpacks/procfile\"},{\"id\":\"paketo-buildpacks/apache-tomcat\",\"version\":\"3.2.0\",\"homepage\":\"https://github.com/paketo-buildpacks/apache-tomcat\"},{\"id\":\"paketo-buildpacks/gradle\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/gradle\"},{\"id\":\"paketo-buildpacks/maven\",\"version\":\"3.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/maven\"},{\"id\":\"paketo-buildpacks/sbt\",\"version\":\"3.6.0\",\"homepage\":\"https://github.com/paketo-buildpacks/sbt\"},{\"id\":\"paketo-buildpacks/bellsoft-liberica\",\"version\":\"6.2.0\",\"homepage\":\"https://github.com/paketo-buildpacks/bellsoft-liberica\"},{\"id\":\"paketo-buildpacks/google-stackdriver\",\"version\":\"2.16.0\",\"homepage\":\"https://github.com/paketo-buildpacks/google-stackdriver\"},{\"id\":\"paketo-buildpacks/image-labels\",\"version\":\"2.0.7\",\"homepage\":\"https://github.com/paketo-buildpacks/image-labels\"},{\"id\":\"paketo-buildpacks/dist-zip\",\"version\":\"2.2.2\",\"homepage\":\"https://github.com/paketo-buildpacks/dist-zip\"},{\"id\":\"paketo-buildpacks/spring-boot\",\"version\":\"3.5.0\",\"homepage\":\"https://github.com/paketo-buildpacks/spring-boot\"},{\"id\":\"paketo-buildpacks/jmx\",\"version\":\"2.1.4\",\"homepage\":\"https://github.com/paketo-buildpacks/jmx\"},{\"id\":\"paketo-buildpacks/leiningen\",\"version\":\"1.2.1\",\"homepage\":\"https://github.com/paketo-buildpacks/leiningen\"}],\"stack\":{\"runImage\":{\"image\":\"cloudfoundry/run:base-cnb\",\"mirrors\":null}},\"lifecycle\":{\"version\":\"0.7.2\",\"api\":{\"buildpack\":\"0.2\",\"platform\":\"0.3\"}},\"createdBy\":{\"name\":\"Pack CLI\",\"version\":\"v0.9.0 (git sha: d42c384a39f367588f2653f2a99702db910e5ad7)\"}}", + "io.buildpacks.buildpack.layers": "{\"org.cloudfoundry.archiveexpanding\":{\"v1.0.102\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:73b1a8ac1f7fca3d545766ce7fd3c56b40a63724ab78e464d71a29da0c6ac31c\"}},\"org.cloudfoundry.azureapplicationinsights\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0a2f7c467efbb8b1ac222f09013b88b68f3c117ec6b6e9dc95564be50f271ab\"}},\"org.cloudfoundry.buildsystem\":{\"v1.2.15\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:3f50d3a0e1a969a9606b59e5295842d731e425108cb349ce6c69a5b30ea1bab9\"}},\"org.cloudfoundry.debug\":{\"v1.2.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:04559213a01cfac69a8d6a6facb58b8681666525c74f605207c40a61a0f4c9b7\"}},\"org.cloudfoundry.dep\":{\"0.0.101\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:6aae3a2d671d369eec34dc9146ef267d06c87461f271fbfbe9136775ecf5dfb8\"}},\"org.cloudfoundry.distzip\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a0715e661e13d7d3ded5bdc068edd01e5b3aa0e2805152f4c8a1428b4e0673df\"}},\"org.cloudfoundry.dotnet-core\":{\"v0.0.6\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.158\",\"optional\":true},{\"id\":\"org.cloudfoundry.icu\",\"version\":\"0.0.43\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-runtime\",\"version\":\"0.0.127\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-aspnet\",\"version\":\"0.0.118\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-sdk\",\"version\":\"0.0.122\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-build\",\"version\":\"0.0.68\",\"optional\":true},{\"id\":\"org.cloudfoundry.dotnet-core-conf\",\"version\":\"0.0.115\"}]}],\"layerDiffID\":\"sha256:aa0effdf787ecfe74d60d6771006717fd1a9ce1ce0a8161624baa61b68120357\"}},\"org.cloudfoundry.dotnet-core-aspnet\":{\"0.0.118\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:a06615b5adc1a3afb7abd524e82f6900a28910927fcf0d4e9b85fd1fcbeb53ad\"}},\"org.cloudfoundry.dotnet-core-build\":{\"0.0.68\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:26d6f1e76275d17860005f7ab9b74fdd2283fcf84e0446bd88d49a6b4e9609f9\"}},\"org.cloudfoundry.dotnet-core-conf\":{\"0.0.115\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:55f7c052cf70c8ca01b8e241c0c5c8a9675599d4904c69bfb961a472e246238d\"}},\"org.cloudfoundry.dotnet-core-runtime\":{\"0.0.127\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:d9958b816a9ad179fca8c18d17c07e9814b152d461c685e1443bec6f990ab990\"}},\"org.cloudfoundry.dotnet-core-sdk\":{\"0.0.122\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:52142799a4b687fe6e5cf397c41064499ea6cc554b94904d46c1acade998e11f\"}},\"org.cloudfoundry.go\":{\"v0.0.4\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.go-mod\",\"version\":\"0.0.89\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go-compiler\",\"version\":\"0.0.105\"},{\"id\":\"org.cloudfoundry.dep\",\"version\":\"0.0.101\"}]}],\"layerDiffID\":\"sha256:352a299d6af4773322ed3643d8f98b01aad6f15d838d1852e52a0a3ca56c6efb\"}},\"org.cloudfoundry.go-compiler\":{\"0.0.105\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:cb21f14e306d94e437c5418d275bcc6efcea6bc9b3d26a400bdf54fa62242c24\"}},\"org.cloudfoundry.go-mod\":{\"0.0.89\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.tiny\"}],\"layerDiffID\":\"sha256:c9da8171f5ca048109ffba5e940e3a7d2db567eda281f92b0eb483173df06add\"}},\"org.cloudfoundry.googlestackdriver\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:ff29efc56c31eeccc79a33c6e4abd7b1ab3547d95e1cf83974af65a493576c41\"}},\"org.cloudfoundry.icu\":{\"0.0.43\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:48063dcdd043f9c88604d10fe9542569be8f8111d46806c96b08d77763ffa347\"}},\"org.cloudfoundry.jdbc\":{\"v1.1.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:a9c9bbbd69c212b7ab3c1a7f03011ccc4d99a6fce1bf1c785325c7bcad789e62\"}},\"org.cloudfoundry.jmx\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:da62dec6eb4ed884952a1b867fd89e3bfe3c510e5c849cc0ac7050ff867a2469\"}},\"org.cloudfoundry.jvmapplication\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c10732392b97c121a78a5f20201c2a5e834a2b8677196cdd49260a489a54fd22\"}},\"org.cloudfoundry.node-engine\":{\"0.0.158\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:70cf83155575fdb607f23ace41e31b1d5cb1c24dbbbf56f71c383b583724d339\"},\"0.0.163\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:11486cb955594f9d43909b60f94209bb6854f502a5a093207b657afbaa38a777\"}},\"org.cloudfoundry.nodejs\":{\"v2.0.8\":{\"api\":\"0.2\",\"order\":[{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.yarn-install\",\"version\":\"0.1.10\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.node-engine\",\"version\":\"0.0.163\"},{\"id\":\"org.cloudfoundry.npm\",\"version\":\"0.1.3\"}]}],\"layerDiffID\":\"sha256:76fe727e4aafc7f56f01282296ab736521c38b9d19c1ae5ebb193f9cd55fa109\"}},\"org.cloudfoundry.npm\":{\"0.1.3\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:243bbd007cb0ee99b704bfe0cf62e1301baa4095ab4c39b01293787a0e4234f1\"}},\"org.cloudfoundry.openjdk\":{\"v1.2.14\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:486b2abf434bb90cf04bab74f2f8bd2eb488ff90632b56eac4bddcbbf02e8151\"}},\"org.cloudfoundry.procfile\":{\"v1.1.12\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b7b78159dfdaa0dd484c58652e02fa6b755abfd0adb88f106d16178144e46f33\"}},\"org.cloudfoundry.springautoreconfiguration\":{\"v1.1.11\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:c185540c10fea822c6db1b987fcfe22b55a4662648124b98475db4c9dcddb2ab\"}},\"org.cloudfoundry.springboot\":{\"v1.2.13\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:b87e68574cc7dccbe974fa760702ef650711036bf144fd9da1f3a2d8f6ac335f\"}},\"org.cloudfoundry.tomcat\":{\"v1.3.18\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"io.buildpacks.stacks.bionic\"},{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"}],\"layerDiffID\":\"sha256:467c0082c57b80b48487a9b8429887c0744ddc5b066b3f7678866bde89b78ab2\"}},\"org.cloudfoundry.yarn-install\":{\"0.1.10\":{\"api\":\"0.2\",\"stacks\":[{\"id\":\"org.cloudfoundry.stacks.cflinuxfs3\"},{\"id\":\"io.buildpacks.stacks.bionic\"}],\"layerDiffID\":\"sha256:6aefa0ba7ce01584b4a531b18e36470298cee3b30ecae0e0c64b532a5cebd6e7\"}}}", + "io.buildpacks.buildpack.order": "[{\"group\":[{\"id\":\"org.cloudfoundry.openjdk\"},{\"id\":\"org.cloudfoundry.buildsystem\",\"optional\":true},{\"id\":\"org.cloudfoundry.jvmapplication\"},{\"id\":\"org.cloudfoundry.tomcat\",\"optional\":true},{\"id\":\"org.cloudfoundry.springboot\",\"optional\":true},{\"id\":\"org.cloudfoundry.distzip\",\"optional\":true},{\"id\":\"org.cloudfoundry.procfile\",\"optional\":true},{\"id\":\"org.cloudfoundry.azureapplicationinsights\",\"optional\":true},{\"id\":\"org.cloudfoundry.debug\",\"optional\":true},{\"id\":\"org.cloudfoundry.googlestackdriver\",\"optional\":true},{\"id\":\"org.cloudfoundry.jdbc\",\"optional\":true},{\"id\":\"org.cloudfoundry.jmx\",\"optional\":true},{\"id\":\"org.cloudfoundry.springautoreconfiguration\",\"optional\":true}]},{\"group\":[{\"id\":\"org.cloudfoundry.nodejs\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.go\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.dotnet-core\"}]},{\"group\":[{\"id\":\"org.cloudfoundry.procfile\"}]}]", + "io.buildpacks.stack.id": "io.buildpacks.stacks.bionic", + "io.buildpacks.stack.mixins": "[\"build:git\",\"build:build-essential\"]" + } + }, + "Architecture": "arm64", + "Os": "linux", + "Variant": "v1", + "Size": 688884758, + "VirtualSize": 688884758, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/6a79181b2840da2706624f46ce5abd4448973b4f951925d5a276b273256063b2/diff:/var/lib/docker/overlay2/429419a203100f60ab16ec6c879fce975c8138422b9053f80accd6124c730fc2/diff:/var/lib/docker/overlay2/6e45ed6daf4f4f3b90fd1ec5fa958775000875661d3e8be3f1af218d192b058d/diff:/var/lib/docker/overlay2/22928ad308cdd55b3fe849d92b6e38c6bc303ba7c9beb8c0e79aa958e16b1864/diff:/var/lib/docker/overlay2/2ca9ec213226a1604f57c8e141d6f1168134a5cb2ccd8f91ee9be5a39036e6bf/diff:/var/lib/docker/overlay2/96ae944fe00ec20cf5b4441b112ebcc9395faaf08108c9ee38c62e1da33af1c8/diff:/var/lib/docker/overlay2/13ee52e300e476e27350c9ac6274dedf26af85c3079b42a41f9dfc92eff57a80/diff:/var/lib/docker/overlay2/223edb4cc62a2ba2b8bda866905a55c4798c6c32e31d22d60e6ed4f3169ce85e/diff:/var/lib/docker/overlay2/a41235cd7277299cb74ead47def3771885948719e24075ea3bf37580f3af7ae2/diff:/var/lib/docker/overlay2/ed0438e8e2c27b9d62ad21a0761237c350a2ffc9e52f47c019e4f627091c832e/diff:/var/lib/docker/overlay2/0c27c8229b31eafc57ab739b44962dcc07b72f3d8950888873ecb3cfd385032f/diff:/var/lib/docker/overlay2/0957cbcca052cd58bcf9a3d945b0e6876b0df79c1c534da1872c3415a019427d/diff:/var/lib/docker/overlay2/b621414d53d71349c07df8ed45e3e04b2e97bfbaf4bf0d86463f46e0f810eeb4/diff:/var/lib/docker/overlay2/ad521bc47f0bb44262358cf47c3d81a544d098494cf24a5b510620d34eb9c353/diff:/var/lib/docker/overlay2/081501d5bfbd927e69c10eb320513c7c0d5f00bea8cf9e55faa90579fd33adf4/diff:/var/lib/docker/overlay2/fb1ba66bee5568f5700c72865d020d4171a62bfdd099c3cc05b9a253d36a35a4/diff:/var/lib/docker/overlay2/06bcc6b3adeca727d554f1a745ee33242dfe1b3c6392023ac947666057303288/diff:/var/lib/docker/overlay2/1c5397d63d893202dffde29013ee826fb695bda26c718ee03ddde376be4da0a3/diff:/var/lib/docker/overlay2/76075fb7fd3c6b3fb116fb3b464e220918e56d94461c61af9a1aff288ebdba60/diff:/var/lib/docker/overlay2/43d1026bb7b618393912ecc9ddf57b604336184d5f8dc70bcf6332b5f08a3e8d/diff:/var/lib/docker/overlay2/ee27d1fba3deaca0556f7bab171cb3368f169011dd132cf335b5308728f6db8f/diff:/var/lib/docker/overlay2/464d3ec8d86ff31dcb5063ea25521368ea8e9c7964f65e15ff5e0e1ecdbe991e/diff:/var/lib/docker/overlay2/a4a80c33c8b78f68bdc9dbd5903cc2ba1d48e78b9a97d43acb018823ece8e6cb/diff:/var/lib/docker/overlay2/6494f2f1693cff8b16d51fa95620eb0bb691a76fb39b5175d953649577791297/diff:/var/lib/docker/overlay2/9d49e146f82eb5fc4fd81613538e9c5f5f95091fbbc8c49729c6c9140ae356de/diff:/var/lib/docker/overlay2/2934818c52bcd017abe000e71342d67fbc9ccb7dbc165ce05e3250e2110229a5/diff:/var/lib/docker/overlay2/651ca06b2bf75e2122855264287fc937f30d2b49229d628909895be7128b4eb6/diff:/var/lib/docker/overlay2/c93bab59be44fa1b66689dc059d26742d00d2e787d06c3236e1f116199c9807e/diff:/var/lib/docker/overlay2/d0a8e2a0c7e0df172f7a8ebe75e2dce371bb6cc65531b06799bc677c5b5e3627/diff:/var/lib/docker/overlay2/7d14bac240e0d7936351e3fac80b7fbe2a209f4de8992091c4f75e41f9627852/diff:/var/lib/docker/overlay2/d6b192ea137a4ae95e309d263ee8c890e35da02aacd9bdcf5adbd4c28a0c0a3f/diff:/var/lib/docker/overlay2/335bfb632ab7723e25fb5dc7b67389e6ec38178ef10bfbf83337501403e61574/diff:/var/lib/docker/overlay2/0293c7e3472da58f51cbdf15fb293ff71e32c1f80f83f00fb09f8941deef5e43/diff:/var/lib/docker/overlay2/55faa8b47bcb0dd29c3836580f451a0461dd499065af9c830beff6e8329ab484/diff:/var/lib/docker/overlay2/afcb6e109c1ba7d71b8a8b7e573d4ce04f22da3fe0ee523359db5cfb95e65bb6/diff:/var/lib/docker/overlay2/b42eefd9bf6629ae9d16e7aba6ba3939d37816aba7a0999f6d639012a3119be1/diff:/var/lib/docker/overlay2/a9832c8f81ee889a622ce4d95d9f4bab2f91d30e18f69bfd7cfc385c781068d4/diff:/var/lib/docker/overlay2/224041c135f13881a98b9e833584bedab81d5650061457f522a1ebd1daa2c77a/diff:/var/lib/docker/overlay2/73dfd4e2075fccb239b3d5e9b33b32b8e410bdc3cd5a620b41346f44cc5c51f7/diff:/var/lib/docker/overlay2/b3924ed7c91730f6714d33c455db888604b59ab093033b3f59ac16ecdd777987/diff:/var/lib/docker/overlay2/e36a32cd0ab20b216a8db1a8a166b17464399e4d587d22504088a7a6ef0a68a4/diff:/var/lib/docker/overlay2/3334e94fe191333b65f571912c0fcfbbf31aeb090a2fb9b4cfdbc32a37c0fe5f/diff", + "MergedDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/merged", + "UpperDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/diff", + "WorkDir": "/var/lib/docker/overlay2/f5d133c5929da8cc8266cbbc3e36f924f4a9c835f943fb436445a26b7e1bcc56/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853", + "sha256:977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f", + "sha256:6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d", + "sha256:16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75", + "sha256:2df36adfe1af661aebb75a0db796b074bb8f861fbc8f98f6f642570692b3b133", + "sha256:f499c7d34e01d860492ef1cc34b7d7e1319b3c3c81ee7d23258b21605b5902ca", + "sha256:c4bf1d4e5d4adb566b173a0769d247f67c5dd8ff90dfdcebd8c7060f1c06caa9", + "sha256:15259abd479904cbe0d8d421e5b05b2e5745e2bf82e62cdd7fb6d3eafbe4168a", + "sha256:6aa3691a73805f608e5fce69fb6bc89aec8362f58a6b4be2682515e9cfa3cc1a", + "sha256:2d6ad1b66f5660dd860c1fe2d90d26398fcfab4dc1c87c3d5e7c0fc24f8d6fb2", + "sha256:ff29efc56c31eeccc79a33c6e4abd7b1ab3547d95e1cf83974af65a493576c41", + "sha256:b87e68574cc7dccbe974fa760702ef650711036bf144fd9da1f3a2d8f6ac335f", + "sha256:04559213a01cfac69a8d6a6facb58b8681666525c74f605207c40a61a0f4c9b7", + "sha256:467c0082c57b80b48487a9b8429887c0744ddc5b066b3f7678866bde89b78ab2", + "sha256:352a299d6af4773322ed3643d8f98b01aad6f15d838d1852e52a0a3ca56c6efb", + "sha256:486b2abf434bb90cf04bab74f2f8bd2eb488ff90632b56eac4bddcbbf02e8151", + "sha256:3f50d3a0e1a969a9606b59e5295842d731e425108cb349ce6c69a5b30ea1bab9", + "sha256:c10732392b97c121a78a5f20201c2a5e834a2b8677196cdd49260a489a54fd22", + "sha256:c185540c10fea822c6db1b987fcfe22b55a4662648124b98475db4c9dcddb2ab", + "sha256:73b1a8ac1f7fca3d545766ce7fd3c56b40a63724ab78e464d71a29da0c6ac31c", + "sha256:da62dec6eb4ed884952a1b867fd89e3bfe3c510e5c849cc0ac7050ff867a2469", + "sha256:76fe727e4aafc7f56f01282296ab736521c38b9d19c1ae5ebb193f9cd55fa109", + "sha256:a9c9bbbd69c212b7ab3c1a7f03011ccc4d99a6fce1bf1c785325c7bcad789e62", + "sha256:b7b78159dfdaa0dd484c58652e02fa6b755abfd0adb88f106d16178144e46f33", + "sha256:aa0effdf787ecfe74d60d6771006717fd1a9ce1ce0a8161624baa61b68120357", + "sha256:a0a2f7c467efbb8b1ac222f09013b88b68f3c117ec6b6e9dc95564be50f271ab", + "sha256:a0715e661e13d7d3ded5bdc068edd01e5b3aa0e2805152f4c8a1428b4e0673df", + "sha256:6aae3a2d671d369eec34dc9146ef267d06c87461f271fbfbe9136775ecf5dfb8", + "sha256:cb21f14e306d94e437c5418d275bcc6efcea6bc9b3d26a400bdf54fa62242c24", + "sha256:c9da8171f5ca048109ffba5e940e3a7d2db567eda281f92b0eb483173df06add", + "sha256:11486cb955594f9d43909b60f94209bb6854f502a5a093207b657afbaa38a777", + "sha256:243bbd007cb0ee99b704bfe0cf62e1301baa4095ab4c39b01293787a0e4234f1", + "sha256:6aefa0ba7ce01584b4a531b18e36470298cee3b30ecae0e0c64b532a5cebd6e7", + "sha256:a06615b5adc1a3afb7abd524e82f6900a28910927fcf0d4e9b85fd1fcbeb53ad", + "sha256:26d6f1e76275d17860005f7ab9b74fdd2283fcf84e0446bd88d49a6b4e9609f9", + "sha256:55f7c052cf70c8ca01b8e241c0c5c8a9675599d4904c69bfb961a472e246238d", + "sha256:d9958b816a9ad179fca8c18d17c07e9814b152d461c685e1443bec6f990ab990", + "sha256:52142799a4b687fe6e5cf397c41064499ea6cc554b94904d46c1acade998e11f", + "sha256:48063dcdd043f9c88604d10fe9542569be8f8111d46806c96b08d77763ffa347", + "sha256:70cf83155575fdb607f23ace41e31b1d5cb1c24dbbbf56f71c383b583724d339", + "sha256:6cf0f8f815d5371cf5c04e7ebf76c62467948d693b8343184d1446036980d261", + "sha256:7cbffcbb09fc5e9d00372e80990016609c09cc3113429ddc951c4a19b1a5ec72", + "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/print-stream-build-log.txt b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/print-stream-build-log.txt index 6fcfc7ee2c..b2d73f7292 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/print-stream-build-log.txt +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/print-stream-build-log.txt @@ -2,7 +2,7 @@ Building image 'docker.io/library/my-app:latest' > Pulling builder image 'docker.io/cnb/builder' .................................................. > Pulled builder image '00000001' - > Pulling run image 'docker.io/cnb/runner' .................................................. + > Pulling run image 'docker.io/cnb/runner' for platform 'linux/arm64/v1' .................................................. > Pulled run image '00000002' > Executing lifecycle version v0.5.0 > Using build cache volume 'pack-abc.cache' diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/run-image-with-platform.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/run-image-with-platform.json new file mode 100644 index 0000000000..0135acd1c0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/run-image-with-platform.json @@ -0,0 +1,98 @@ +{ + "Id": "sha256:1332879bc8e38793a45ebe5a750f2a1c35df07ec2aa9c18f694644a9de77359b", + "RepoTags": [ + "cloudfoundry/run:base-cnb" + ], + "RepoDigests": [ + "cloudfoundry/run@sha256:fb5ecb90a42b2067a859aab23fc1f5e9d9c2589d07ba285608879e7baa415aad" + ], + "Parent": "", + "Comment": "", + "Created": "2020-03-20T20:18:18.117972538Z", + "Container": "91d1af87c3bb6163cd9c7cb21e6891cd25f5fa3c7417779047776e288c0bc234", + "ContainerConfig": { + "Hostname": "91d1af87c3bb", + "Domainname": "", + "User": "1000:1000", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "LABEL io.buildpacks.stack.id=io.buildpacks.stacks.bionic" + ], + "ArgsEscaped": true, + "Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.stack.id": "io.buildpacks.stacks.bionic" + } + }, + "DockerVersion": "18.09.6", + "Author": "", + "Config": { + "Hostname": "", + "Domainname": "", + "User": "1000:1000", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/bin/bash" + ], + "ArgsEscaped": true, + "Image": "sha256:fbe314bcb23f15a2a09603b6620acd67c332fd08fbf2a7bc3db8fb2f5078d994", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": { + "io.buildpacks.stack.id": "io.buildpacks.stacks.bionic" + } + }, + "Architecture": "arm64", + "Os": "linux", + "Variant": "v1", + "Size": 71248531, + "VirtualSize": 71248531, + "GraphDriver": { + "Data": { + "LowerDir": "/var/lib/docker/overlay2/17f0a4530fbc3e2982f9dc8feb8c8ddc124473bdd50130dae20856ac597d82dd/diff:/var/lib/docker/overlay2/73dfd4e2075fccb239b3d5e9b33b32b8e410bdc3cd5a620b41346f44cc5c51f7/diff:/var/lib/docker/overlay2/b3924ed7c91730f6714d33c455db888604b59ab093033b3f59ac16ecdd777987/diff:/var/lib/docker/overlay2/e36a32cd0ab20b216a8db1a8a166b17464399e4d587d22504088a7a6ef0a68a4/diff:/var/lib/docker/overlay2/3334e94fe191333b65f571912c0fcfbbf31aeb090a2fb9b4cfdbc32a37c0fe5f/diff", + "MergedDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/merged", + "UpperDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/diff", + "WorkDir": "/var/lib/docker/overlay2/8d3f9e3c00bc5072f8051ec7884500ca394f2331d8bcc9452f68d04531f50f82/work" + }, + "Name": "overlay2" + }, + "RootFS": { + "Type": "layers", + "Layers": [ + "sha256:c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853", + "sha256:977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f", + "sha256:6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d", + "sha256:16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75", + "sha256:c1daeb79beb276c7441d9a1d7281433e9a7edb9f652b8996ecc62b51e88a47b2", + "sha256:eb195d29dc1aa6e4239f00e7868deebc5ac12bebe76104e0b774c1ef29ca78e3" + ] + }, + "Metadata": { + "LastTagTime": "0001-01-01T00:00:00Z" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-config.json index 395bbcda70..fedefec5d7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-config.json +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-config.json @@ -1,5 +1,5 @@ { - "config": { + "Config": { "Hostname": "", "Domainname": "", "User": "vcap", @@ -25,8 +25,8 @@ "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" } }, - "created": "1980-01-01T00:00:01Z", - "history": [ + "Created": "1980-01-01T00:00:01Z", + "History": [ { }, @@ -169,8 +169,10 @@ } ], - "os": "linux", - "rootfs": { + "Architecture": "amd64", + "Os": "linux", + "Variant": "v1", + "RootFS": { "diff_ids": [ "sha256:733a8e5ce32984099ef675fce04730f6e2a6dcfdf5bd292fea01a8f936265342", "sha256:7755b972f0b4f49de73ef5114fb3ba9c69d80f217e80da99f56f0d0a5dcb3d70", diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-manifest.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-manifest.json index 4830d4e8e1..129b9cb908 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-manifest.json +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image-archive-manifest.json @@ -1,6 +1,6 @@ [ { - "Config": "682f8d24b9d9c313d1190a0e955dcb5e65ec9beea40420999839c6f0cbb38382.json", + "Config": "416c76dc7f691f91e80516ff039e056f32f996b59af4b1cb8114e6ae8171a374.json", "Layers": [ "blank_0", "blank_1", diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image.json index 8fbaf31556..901e3b90f5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image.json +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/type/image.json @@ -72,9 +72,10 @@ "io.buildpacks.stack.id": "org.cloudfoundry.stacks.cflinuxfs3" } }, - "Architecture": "amd64", "Os": "linux", - "Size": 1559461360, + "Architecture": "amd64", + "Variant": "v1", + "Size": 1559461360, "VirtualSize": 1559461360, "GraphDriver": { "Data": { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index f34eff25a4..3c84395f8f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -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 + } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageRegistryIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageRegistryIntegrationTests.java index 8d0e8dd00f..397bb4dacd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageRegistryIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageRegistryIntegrationTests.java @@ -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); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageOnLinuxArmWithImagePlatformLinuxArm.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageOnLinuxArmWithImagePlatformLinuxArm.gradle new file mode 100644 index 0000000000..5fa10d232d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageOnLinuxArmWithImagePlatformLinuxArm.gradle @@ -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" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenBuildingOnLinuxAmdWithImagePlatformLinuxArm.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenBuildingOnLinuxAmdWithImagePlatformLinuxArm.gradle new file mode 100644 index 0000000000..5fa10d232d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/dockerTest/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWhenBuildingOnLinuxAmdWithImagePlatformLinuxArm.gradle @@ -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" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc index 1e8f22f04c..7c49ee7715 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc @@ -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` diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index ce11a93a4a..4182e0c23c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -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 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 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; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index 348af9d7a2..199fe18aad 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -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")); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/java/org/springframework/boot/maven/BuildImageRegistryIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/java/org/springframework/boot/maven/BuildImageRegistryIntegrationTests.java index 707462346e..872056cb9b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/java/org/springframework/boot/maven/BuildImageRegistryIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/java/org/springframework/boot/maven/BuildImageRegistryIntegrationTests.java @@ -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); }); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/java/org/springframework/boot/maven/BuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/java/org/springframework/boot/maven/BuildImageTests.java index a618bf525d..8dcc8b8be6 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/java/org/springframework/boot/maven/BuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/java/org/springframework/boot/maven/BuildImageTests.java @@ -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 { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/projects/build-image-platform-linux-arm/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/projects/build-image-platform-linux-arm/pom.xml new file mode 100644 index 0000000000..8b477431b5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/projects/build-image-platform-linux-arm/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-platform-linux-arm + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image-no-fork + + + + ghcr.io/spring-io/spring-boot-cnb-test-builder:0.0.1 + paketobuildpacks/run-jammy-tiny + + ghcr.io/spring-io/spring-boot-test-info:0.0.1 + + linux/arm64 + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/projects/build-image-platform-linux-arm/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/projects/build-image-platform-linux-arm/src/main/java/org/test/SampleApplication.java new file mode 100644 index 0000000000..922c0107e8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/dockerTest/projects/build-image-platform-linux-arm/src/main/java/org/test/SampleApplication.java @@ -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 + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc index 16683a14be..fdb3436c05 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc @@ -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`) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java index 40ac762c58..a5ae26a2a9 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java @@ -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)); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java index 0256127d75..a72373c06b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -83,6 +83,8 @@ public class Image { List 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 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; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index 5cdd1cf4b1..de54da8efd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -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());