From 6230392c4c457ffcf52d780b58f961c86d944bf4 Mon Sep 17 00:00:00 2001 From: hojooo Date: Sun, 21 Sep 2025 23:43:57 +0900 Subject: [PATCH 1/8] Respect image platform when exporting buildpack layers Ensure the builder propagates the requested image platform to DockerApi.exportLayers so Docker 1.41+ saves layers for the pulled architecture rather than the host default. Add platform-aware tests issue: #46665 Signed-off-by: hojooo --- .../buildpack/platform/build/Builder.java | 2 +- .../buildpack/platform/docker/DockerApi.java | 17 +++++++++- .../platform/docker/DockerApiTests.java | 32 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index 4e4aab001106..59ebdc82ebbc 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -355,7 +355,7 @@ public Image fetchImage(ImageReference reference, ImageType imageType) throws IO @Override public void exportImageLayers(ImageReference reference, IOBiConsumer exports) throws IOException { - Builder.this.docker.image().exportLayers(reference, exports); + Builder.this.docker.image().exportLayers(reference, this.imageFetcher.defaultPlatform, exports); } } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index a08789a94e95..63ac788130bb 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -311,9 +311,24 @@ public void load(ImageArchive archive, UpdateListener list */ public void exportLayers(ImageReference reference, IOBiConsumer exports) throws IOException { + exportLayers(reference, null, exports); + } + + /** + * Export the layers of an image as {@link TarArchive TarArchives}. + * @param reference the reference to export + * @param platform the platform (os/architecture/variant) of the image to export + * @param exports a consumer to receive the layers (contents can only be accessed + * during the callback) + * @throws IOException on IO error + */ + public void exportLayers(ImageReference reference, @Nullable ImagePlatform platform, + IOBiConsumer exports) throws IOException { Assert.notNull(reference, "'reference' must not be null"); Assert.notNull(exports, "'exports' must not be null"); - URI uri = buildUrl("/images/" + reference + "/get"); + URI uri = (platform != null) + ? buildUrl(PLATFORM_API_VERSION, "/images/" + reference + "/get", "platform", platform) + : buildUrl("/images/" + reference + "/get"); try (Response response = http().get(uri)) { try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, response.getContent())) { exportedImageTar.exportLayers(exports); diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index c06270461f26..ad1085cc4037 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -447,6 +447,38 @@ void exportLayersWithSymlinksExportsLayerTars() throws Exception { .containsExactly("/cnb/stack.toml"); } + @Test + void exportLayersWithPlatformExportsLayerTars() throws Exception { + ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base"); + ImagePlatform platform = ImagePlatform.of("linux/amd64"); + URI exportUri = new URI( + PLATFORM_IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/get?platform=linux%2Famd64"); + given(DockerApiTests.this.http.head(eq(new URI(PING_URL)))) + .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41"))); + given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export.tar")); + MultiValueMap contents = new LinkedMultiValueMap<>(); + this.api.exportLayers(reference, platform, (name, archive) -> { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + archive.writeTo(out); + try (TarArchiveInputStream in = new TarArchiveInputStream( + new ByteArrayInputStream(out.toByteArray()))) { + TarArchiveEntry entry = in.getNextEntry(); + while (entry != null) { + contents.add(name, entry.getName()); + entry = in.getNextEntry(); + } + } + }); + assertThat(contents).hasSize(3) + .containsKeys("70bb7a3115f3d5c01099852112c7e05bf593789e510468edb06b6a9a11fa3b73/layer.tar", + "74a9a50ece13c025cf10e9110d9ddc86c995079c34e2a22a28d1a3d523222c6e/layer.tar", + "a69532b5b92bb891fbd9fa1a6b3af9087ea7050255f59ba61a796f8555ecd783/layer.tar"); + assertThat(contents.get("70bb7a3115f3d5c01099852112c7e05bf593789e510468edb06b6a9a11fa3b73/layer.tar")) + .containsExactly("/cnb/order.toml"); + assertThat(contents.get("74a9a50ece13c025cf10e9110d9ddc86c995079c34e2a22a28d1a3d523222c6e/layer.tar")) + .containsExactly("/cnb/stack.toml"); + } + @Test void tagWhenReferenceIsNullThrowsException() { ImageReference tag = ImageReference.of("localhost:5000/ubuntu"); From 9319625cc40b23d16e3600fffa5461425a11806d Mon Sep 17 00:00:00 2001 From: hojooo Date: Sun, 21 Sep 2025 23:47:25 +0900 Subject: [PATCH 2/8] Respect pulled digest when inspecting multi-arch images Capture the digest emitted during the pull stream and inspect name@digest so the newly pulled manifest is always used. issue: #46674. Signed-off-by: hojooo --- .../boot/buildpack/platform/docker/DockerApi.java | 8 +++++++- .../boot/buildpack/platform/docker/DockerApiTests.java | 9 ++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 63ac788130bb..cda297cccf63 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -239,7 +239,9 @@ public Image pull(ImageReference reference, @Nullable ImagePlatform platform, listener.onUpdate(event); }); } - return inspect((platform != null) ? PLATFORM_API_VERSION : API_VERSION, reference); + String digest = digestCapture.getDigest(); + ImageReference inspectReference = (digest != null) ? reference.withDigest(digest) : reference; + return inspect((platform != null) ? PLATFORM_API_VERSION : API_VERSION, inspectReference); } finally { listener.onFinish(); @@ -564,6 +566,10 @@ public void onUpdate(ProgressUpdateEvent event) { } } + private @Nullable String getDigest() { + return this.digest; + } + } /** diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index ad1085cc4037..e22a1b80241e 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -207,7 +207,8 @@ void pullWhenListenerIsNullThrowsException() { void pullPullsImageAndProducesEvents() throws Exception { ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base"); URI createUri = new URI(IMAGES_URL + "/create?fromImage=docker.io%2Fpaketobuildpacks%2Fbuilder%3Abase"); - URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/json"); + URI imageUri = new URI(IMAGES_URL + + "/docker.io/paketobuildpacks/builder@sha256:4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30/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, null, this.pullListener); @@ -222,7 +223,8 @@ void pullPullsImageAndProducesEvents() throws Exception { void pullWithRegistryAuthPullsImageAndProducesEvents() throws Exception { ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base"); URI createUri = new URI(IMAGES_URL + "/create?fromImage=docker.io%2Fpaketobuildpacks%2Fbuilder%3Abase"); - URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/json"); + URI imageUri = new URI(IMAGES_URL + + "/docker.io/paketobuildpacks/builder@sha256:4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30/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, null, this.pullListener, "auth token"); @@ -239,7 +241,8 @@ void pullWithPlatformPullsImageAndProducesEvents() throws Exception { ImagePlatform platform = ImagePlatform.of("linux/arm64/v1"); URI createUri = new URI(PLATFORM_IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase&platform=linux%2Farm64%2Fv1"); - URI imageUri = new URI(PLATFORM_IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json"); + URI imageUri = new URI(PLATFORM_IMAGES_URL + + "/gcr.io/paketo-buildpacks/builder@sha256:4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30/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")); From 174fdb36dcaaea5ed52088a124a7d1158567acdf Mon Sep 17 00:00:00 2001 From: hojooo Date: Tue, 28 Oct 2025 16:36:35 +0900 Subject: [PATCH 3/8] Support platform queries for image inspect and export Signed-off-by: hojooo --- .../buildpack/platform/docker/DockerApi.java | 39 +++++++++++++++---- .../platform/docker/type/ImagePlatform.java | 27 +++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index cda297cccf63..fecdbc287b5b 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -68,6 +68,10 @@ public class DockerApi { static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41); + static final ApiVersion INSPECT_PLATFORM_API_VERSION = ApiVersion.of(1, 49); + + static final ApiVersion EXPORT_PLATFORM_API_VERSION = ApiVersion.of(1, 51); + static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0); static final String API_VERSION_HEADER_NAME = "API-Version"; @@ -239,9 +243,12 @@ public Image pull(ImageReference reference, @Nullable ImagePlatform platform, listener.onUpdate(event); }); } - String digest = digestCapture.getDigest(); - ImageReference inspectReference = (digest != null) ? reference.withDigest(digest) : reference; - return inspect((platform != null) ? PLATFORM_API_VERSION : API_VERSION, inspectReference); + ApiVersion callVersion = API_VERSION; + if (platform != null) { + callVersion = (getApiVersion().supports(INSPECT_PLATFORM_API_VERSION)) + ? INSPECT_PLATFORM_API_VERSION : PLATFORM_API_VERSION; + } + return inspect(callVersion, reference, platform); } finally { listener.onFinish(); @@ -328,9 +335,20 @@ public void exportLayers(ImageReference reference, @Nullable ImagePlatform platf IOBiConsumer exports) throws IOException { Assert.notNull(reference, "'reference' must not be null"); Assert.notNull(exports, "'exports' must not be null"); - URI uri = (platform != null) - ? buildUrl(PLATFORM_API_VERSION, "/images/" + reference + "/get", "platform", platform) - : buildUrl("/images/" + reference + "/get"); + URI uri; + if (platform != null) { + if (getApiVersion().supports(EXPORT_PLATFORM_API_VERSION)) { + uri = buildUrl(EXPORT_PLATFORM_API_VERSION, "/images/" + reference + "/get", "platform", + platform.toString()); + } + else { + // Platform selection for /images/{ref}/get is supported from 1.51 + uri = buildUrl("/images/" + reference + "/get"); + } + } + else { + uri = buildUrl("/images/" + reference + "/get"); + } try (Response response = http().get(uri)) { try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, response.getContent())) { exportedImageTar.exportLayers(exports); @@ -362,8 +380,15 @@ public Image inspect(ImageReference reference) throws IOException { } private Image inspect(ApiVersion apiVersion, ImageReference reference) throws IOException { + return inspect(apiVersion, reference, null); + } + + private Image inspect(ApiVersion apiVersion, ImageReference reference, @Nullable ImagePlatform platform) + throws IOException { Assert.notNull(reference, "'reference' must not be null"); - URI imageUri = buildUrl(apiVersion, "/images/" + reference + "/json"); + URI imageUri = (platform != null) + ? buildUrl(apiVersion, "/images/" + reference + "/json", "platform", platform.toQueryParameter(getApiVersion())) + : buildUrl(apiVersion, "/images/" + reference + "/json"); try (Response response = http().get(imageUri)) { return Image.of(response.getContent()); } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java index 75bfbfbcaafb..5069f8a1f030 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java @@ -101,4 +101,31 @@ public static ImagePlatform from(Image image) { return new ImagePlatform(image.getOs(), image.getArchitecture(), image.getVariant()); } + /** + * Return the value to use for the Docker API {@code platform} query parameter for the + * given API version. + * + * For API versions that support JSON (1.49 and above), this method returns a JSON + * object in the form {@code {"os":"...","architecture":"...","variant":"..."}}. + * For lower API versions, the legacy string representation {@code os[/arch[/variant]]} + * is returned. + * @param apiVersion the Docker API version to target + * @return the query parameter value to use for {@code platform} + */ + public String toQueryParameter(ApiVersion apiVersion) { + Assert.notNull(apiVersion, "'apiVersion' must not be null"); + if (apiVersion.supports(ApiVersion.of(1, 49))) { + StringBuilder json = new StringBuilder("{"); + json.append("\"os\":\"").append(this.os).append("\""); + if (this.architecture != null && !this.architecture.isEmpty()) { + json.append(",\"architecture\":\"").append(this.architecture).append("\""); + } + if (this.variant != null && !this.variant.isEmpty()) { + json.append(",\"variant\":\"").append(this.variant).append("\""); + } + json.append("}"); + return json.toString(); + } + return toString(); + } } From 1ff9c5b8e86e2c81df281aadf5f939bd5f4e4fe4 Mon Sep 17 00:00:00 2001 From: hojooo Date: Tue, 4 Nov 2025 17:48:00 +0900 Subject: [PATCH 4/8] Fix exportLayers to use JSON-encoded platform format Signed-off-by: hojooo --- .../buildpack/platform/docker/DockerApi.java | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index fecdbc287b5b..c78a6e7e31b8 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -68,9 +68,9 @@ public class DockerApi { static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41); - static final ApiVersion INSPECT_PLATFORM_API_VERSION = ApiVersion.of(1, 49); + static final ApiVersion EXPORT_PLATFORM_API_VERSION = ApiVersion.of(1, 48); - static final ApiVersion EXPORT_PLATFORM_API_VERSION = ApiVersion.of(1, 51); + static final ApiVersion INSPECT_PLATFORM_API_VERSION = ApiVersion.of(1, 49); static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0); @@ -243,12 +243,17 @@ public Image pull(ImageReference reference, @Nullable ImagePlatform platform, listener.onUpdate(event); }); } - ApiVersion callVersion = API_VERSION; if (platform != null) { - callVersion = (getApiVersion().supports(INSPECT_PLATFORM_API_VERSION)) - ? INSPECT_PLATFORM_API_VERSION : PLATFORM_API_VERSION; + if (getApiVersion().supports(INSPECT_PLATFORM_API_VERSION)) { + return inspect(INSPECT_PLATFORM_API_VERSION, reference, platform); + } + String digest = digestCapture.getDigest(); + if (digest != null) { + ImageReference digestRef = reference.withDigest(digest); + return inspect(API_VERSION, digestRef); + } } - return inspect(callVersion, reference, platform); + return inspect(API_VERSION, reference); } finally { listener.onFinish(); @@ -335,19 +340,12 @@ public void exportLayers(ImageReference reference, @Nullable ImagePlatform platf IOBiConsumer exports) throws IOException { Assert.notNull(reference, "'reference' must not be null"); Assert.notNull(exports, "'exports' must not be null"); - URI uri; + URI uri = buildUrl("/images/" + reference + "/get"); if (platform != null) { if (getApiVersion().supports(EXPORT_PLATFORM_API_VERSION)) { uri = buildUrl(EXPORT_PLATFORM_API_VERSION, "/images/" + reference + "/get", "platform", - platform.toString()); + platform.toQueryParameter(getApiVersion())); } - else { - // Platform selection for /images/{ref}/get is supported from 1.51 - uri = buildUrl("/images/" + reference + "/get"); - } - } - else { - uri = buildUrl("/images/" + reference + "/get"); } try (Response response = http().get(uri)) { try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, response.getContent())) { From eeaab6d13abcb2fea3c7a2e81a6bd22135d8a451 Mon Sep 17 00:00:00 2001 From: hojooo Date: Tue, 4 Nov 2025 18:26:22 +0900 Subject: [PATCH 5/8] Simplify format handling Signed-off-by: hojooo --- .../buildpack/platform/docker/DockerApi.java | 4 +- .../platform/docker/type/ImagePlatform.java | 37 +++++++------------ 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index c78a6e7e31b8..367eaf15e17b 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -344,7 +344,7 @@ public void exportLayers(ImageReference reference, @Nullable ImagePlatform platf if (platform != null) { if (getApiVersion().supports(EXPORT_PLATFORM_API_VERSION)) { uri = buildUrl(EXPORT_PLATFORM_API_VERSION, "/images/" + reference + "/get", "platform", - platform.toQueryParameter(getApiVersion())); + platform.toJson()); } } try (Response response = http().get(uri)) { @@ -385,7 +385,7 @@ private Image inspect(ApiVersion apiVersion, ImageReference reference, @Nullable throws IOException { Assert.notNull(reference, "'reference' must not be null"); URI imageUri = (platform != null) - ? buildUrl(apiVersion, "/images/" + reference + "/json", "platform", platform.toQueryParameter(getApiVersion())) + ? buildUrl(apiVersion, "/images/" + reference + "/json", "platform", platform.toJson()) : buildUrl(apiVersion, "/images/" + reference + "/json"); try (Response response = http().get(imageUri)) { return Image.of(response.getContent()); diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java index 5069f8a1f030..16eb2115bbe6 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java @@ -102,30 +102,21 @@ public static ImagePlatform from(Image image) { } /** - * Return the value to use for the Docker API {@code platform} query parameter for the - * given API version. - * - * For API versions that support JSON (1.49 and above), this method returns a JSON - * object in the form {@code {"os":"...","architecture":"...","variant":"..."}}. - * For lower API versions, the legacy string representation {@code os[/arch[/variant]]} - * is returned. - * @param apiVersion the Docker API version to target - * @return the query parameter value to use for {@code platform} + * Return a JSON-encoded representation of this platform for use with Docker Engine + * API 1.48+ endpoints that require the platform parameter in JSON format + * (e.g., image inspect and export operations). + * @return a JSON object in the form {@code {"os":"...","architecture":"...","variant":"..."}} */ - public String toQueryParameter(ApiVersion apiVersion) { - Assert.notNull(apiVersion, "'apiVersion' must not be null"); - if (apiVersion.supports(ApiVersion.of(1, 49))) { - StringBuilder json = new StringBuilder("{"); - json.append("\"os\":\"").append(this.os).append("\""); - if (this.architecture != null && !this.architecture.isEmpty()) { - json.append(",\"architecture\":\"").append(this.architecture).append("\""); - } - if (this.variant != null && !this.variant.isEmpty()) { - json.append(",\"variant\":\"").append(this.variant).append("\""); - } - json.append("}"); - return json.toString(); + public String toJson() { + StringBuilder json = new StringBuilder("{"); + json.append("\"os\":\"").append(this.os).append("\""); + if (this.architecture != null && !this.architecture.isEmpty()) { + json.append(",\"architecture\":\"").append(this.architecture).append("\""); } - return toString(); + if (this.variant != null && !this.variant.isEmpty()) { + json.append(",\"variant\":\"").append(this.variant).append("\""); + } + json.append("}"); + return json.toString(); } } From 772add5a0c6caa6d6d616bf45cd96007bb1a3080 Mon Sep 17 00:00:00 2001 From: hojooo Date: Wed, 5 Nov 2025 01:02:43 +0900 Subject: [PATCH 6/8] Fix DockerApiTests and fallback api version Signed-off-by: hojooo --- .../buildpack/platform/docker/DockerApi.java | 2 +- .../platform/docker/DockerApiTests.java | 38 +------------------ 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 367eaf15e17b..5b5efefd6d82 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -250,7 +250,7 @@ public Image pull(ImageReference reference, @Nullable ImagePlatform platform, String digest = digestCapture.getDigest(); if (digest != null) { ImageReference digestRef = reference.withDigest(digest); - return inspect(API_VERSION, digestRef); + return inspect(PLATFORM_API_VERSION, digestRef); } } return inspect(API_VERSION, reference); diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index e22a1b80241e..b6571d4a403d 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -207,8 +207,7 @@ void pullWhenListenerIsNullThrowsException() { void pullPullsImageAndProducesEvents() throws Exception { ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base"); URI createUri = new URI(IMAGES_URL + "/create?fromImage=docker.io%2Fpaketobuildpacks%2Fbuilder%3Abase"); - URI imageUri = new URI(IMAGES_URL - + "/docker.io/paketobuildpacks/builder@sha256:4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30/json"); + URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/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, null, this.pullListener); @@ -223,8 +222,7 @@ void pullPullsImageAndProducesEvents() throws Exception { void pullWithRegistryAuthPullsImageAndProducesEvents() throws Exception { ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base"); URI createUri = new URI(IMAGES_URL + "/create?fromImage=docker.io%2Fpaketobuildpacks%2Fbuilder%3Abase"); - URI imageUri = new URI(IMAGES_URL - + "/docker.io/paketobuildpacks/builder@sha256:4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30/json"); + URI imageUri = new URI(IMAGES_URL + "/docker.io/paketobuildpacks/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, null, this.pullListener, "auth token"); @@ -450,38 +448,6 @@ void exportLayersWithSymlinksExportsLayerTars() throws Exception { .containsExactly("/cnb/stack.toml"); } - @Test - void exportLayersWithPlatformExportsLayerTars() throws Exception { - ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base"); - ImagePlatform platform = ImagePlatform.of("linux/amd64"); - URI exportUri = new URI( - PLATFORM_IMAGES_URL + "/docker.io/paketobuildpacks/builder:base/get?platform=linux%2Famd64"); - given(DockerApiTests.this.http.head(eq(new URI(PING_URL)))) - .willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41"))); - given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export.tar")); - MultiValueMap contents = new LinkedMultiValueMap<>(); - this.api.exportLayers(reference, platform, (name, archive) -> { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - archive.writeTo(out); - try (TarArchiveInputStream in = new TarArchiveInputStream( - new ByteArrayInputStream(out.toByteArray()))) { - TarArchiveEntry entry = in.getNextEntry(); - while (entry != null) { - contents.add(name, entry.getName()); - entry = in.getNextEntry(); - } - } - }); - assertThat(contents).hasSize(3) - .containsKeys("70bb7a3115f3d5c01099852112c7e05bf593789e510468edb06b6a9a11fa3b73/layer.tar", - "74a9a50ece13c025cf10e9110d9ddc86c995079c34e2a22a28d1a3d523222c6e/layer.tar", - "a69532b5b92bb891fbd9fa1a6b3af9087ea7050255f59ba61a796f8555ecd783/layer.tar"); - assertThat(contents.get("70bb7a3115f3d5c01099852112c7e05bf593789e510468edb06b6a9a11fa3b73/layer.tar")) - .containsExactly("/cnb/order.toml"); - assertThat(contents.get("74a9a50ece13c025cf10e9110d9ddc86c995079c34e2a22a28d1a3d523222c6e/layer.tar")) - .containsExactly("/cnb/stack.toml"); - } - @Test void tagWhenReferenceIsNullThrowsException() { ImageReference tag = ImageReference.of("localhost:5000/ubuntu"); From 3bd8188d079eb4276384f97d0b912c6e49239083 Mon Sep 17 00:00:00 2001 From: hojooo Date: Thu, 6 Nov 2025 22:05:34 +0900 Subject: [PATCH 7/8] Fix Selected Index Digest to Manifest Digest Signed-off-by: hojooo --- .../buildpack/platform/build/Builder.java | 35 ++++++++++--- .../buildpack/platform/docker/DockerApi.java | 49 ++++++++++++------- .../buildpack/platform/docker/type/Image.java | 45 +++++++++++++++++ 3 files changed, 104 insertions(+), 25 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index 59ebdc82ebbc..c6d77782b0cc 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -105,14 +105,22 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio Assert.notNull(request, "'request' must not be null"); this.log.start(request); validateBindings(request.getBindings()); - PullPolicy pullPolicy = request.getPullPolicy(); - ImageFetcher imageFetcher = new ImageFetcher(this.dockerConfiguration.builderRegistryAuthentication(), - pullPolicy, request.getImagePlatform()); + PullPolicy pullPolicy = request.getPullPolicy(); + ImagePlatform requestedPlatform = request.getImagePlatform(); + ImageFetcher imageFetcher = new ImageFetcher(this.dockerConfiguration.builderRegistryAuthentication(), + pullPolicy, requestedPlatform); Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder()); BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage); request = withRunImageIfNeeded(request, builderMetadata); - Assert.state(request.getRunImage() != null, "'request.getRunImage()' must not be null"); - Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage()); + ImageReference imageReference = request.getRunImage(); + Assert.state(imageReference != null, "'imageReference' must not be null"); + Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, imageReference); + String digest = this.docker.image().resolveManifestDigest(imageReference, requestedPlatform); + if (StringUtils.hasText(digest)) { + imageReference = imageReference.withDigest(digest); + runImage = imageFetcher.fetchImage(ImageType.RUNNER, imageReference); + } + request = request.withRunImage(imageReference); assertStackIdsMatch(runImage, builderImage); BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv()); BuildpackLayersMetadata buildpackLayersMetadata = BuildpackLayersMetadata.fromImage(builderImage); @@ -355,8 +363,21 @@ public Image fetchImage(ImageReference reference, ImageType imageType) throws IO @Override public void exportImageLayers(ImageReference reference, IOBiConsumer exports) throws IOException { - Builder.this.docker.image().exportLayers(reference, this.imageFetcher.defaultPlatform, exports); - } + try { + ImageReference pinned = reference; + String digest = Builder.this.docker.image().resolveManifestDigest(reference, + this.imageFetcher.defaultPlatform); + if (org.springframework.util.StringUtils.hasText(digest)) { + pinned = pinned.withDigest(digest); + } + if (!pinned.equals(reference)) { + Builder.this.docker.image().exportLayers(pinned, null, exports); + } + } + catch (Exception ex) { + Builder.this.docker.image().exportLayers(reference, this.imageFetcher.defaultPlatform, exports); + } + } } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 5b5efefd6d82..ae23d7440de0 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -243,16 +243,9 @@ public Image pull(ImageReference reference, @Nullable ImagePlatform platform, listener.onUpdate(event); }); } - if (platform != null) { - if (getApiVersion().supports(INSPECT_PLATFORM_API_VERSION)) { - return inspect(INSPECT_PLATFORM_API_VERSION, reference, platform); - } - String digest = digestCapture.getDigest(); - if (digest != null) { - ImageReference digestRef = reference.withDigest(digest); - return inspect(PLATFORM_API_VERSION, digestRef); - } - } + if (platform != null) { + return inspect(INSPECT_PLATFORM_API_VERSION, reference, platform); + } return inspect(API_VERSION, reference); } finally { @@ -342,10 +335,8 @@ public void exportLayers(ImageReference reference, @Nullable ImagePlatform platf Assert.notNull(exports, "'exports' must not be null"); URI uri = buildUrl("/images/" + reference + "/get"); if (platform != null) { - if (getApiVersion().supports(EXPORT_PLATFORM_API_VERSION)) { - uri = buildUrl(EXPORT_PLATFORM_API_VERSION, "/images/" + reference + "/get", "platform", - platform.toJson()); - } + uri = buildUrl(EXPORT_PLATFORM_API_VERSION, "/images/" + reference + "/get", "platform", + platform.toJson()); } try (Response response = http().get(uri)) { try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, response.getContent())) { @@ -354,6 +345,32 @@ public void exportLayers(ImageReference reference, @Nullable ImagePlatform platf } } + /** + * Resolve an image manifest digest via Docker inspect. + * If {@code platform} is provided, performs a platform-aware inspect. + * Preference order: {@code Descriptor.digest} then first {@code RepoDigest}. + * Returns an empty string if no digest can be determined. + * @param reference image reference + * @param platform desired platform + * @return resolved digest or empty string + * @throws IOException on IO error + */ + public String resolveManifestDigest(ImageReference reference, @Nullable ImagePlatform platform) + throws IOException { + Assert.notNull(reference, "'reference' must not be null"); + Image image = inspect(API_VERSION, reference); + if (platform != null) { + image = inspect(INSPECT_PLATFORM_API_VERSION, reference, platform); + } + Image.Descriptor descriptor = image.getDescriptor(); + if (descriptor != null && StringUtils.hasText(descriptor.getDigest())) { + return descriptor.getDigest(); + } + List repoDigests = image.getDigests(); + String digest = repoDigests.isEmpty() ? "" : repoDigests.get(0); + return digest.substring(digest.indexOf('@') + 1); + } + /** * Remove a specific image. * @param reference the reference the remove @@ -589,10 +606,6 @@ public void onUpdate(ProgressUpdateEvent event) { } } - private @Nullable String getDigest() { - return this.digest; - } - } /** diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java index 0ad0c67d5d77..947133c94e45 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import org.jspecify.annotations.Nullable; import tools.jackson.databind.JsonNode; @@ -52,6 +53,8 @@ public class Image extends MappedObject { private final @Nullable String created; + private final @Nullable Descriptor descriptor; + Image(JsonNode node) { super(node, MethodHandles.lookup()); this.digests = childrenAt("/RepoDigests", JsonNode::asString); @@ -61,6 +64,8 @@ public class Image extends MappedObject { this.architecture = valueAt("/Architecture", String.class); this.variant = valueAt("/Variant", String.class); this.created = valueAt("/Created", String.class); + JsonNode descriptorNode = getNode().path("Descriptor"); + this.descriptor = (descriptorNode.isMissingNode() || descriptorNode.isNull()) ? null : new Descriptor(descriptorNode); } private List extractLayers(String @Nullable [] layers) { @@ -126,6 +131,46 @@ public String getOs() { return this.created; } + /** + * Return the descriptor for this image as reported by Docker Engine inspect. + * @return the image descriptor or {@code null} + */ + public @Nullable Descriptor getDescriptor() { + return this.descriptor; + } + + /** + * Descriptor details as reported by the Docker Engine inspect response. + */ + public static final class Descriptor extends MappedObject { + + private final @Nullable String mediaType; + + private final String digest; + + private final @Nullable Long size; + + Descriptor(JsonNode node) { + super(node, MethodHandles.lookup()); + this.mediaType = valueAt("/mediaType", String.class); + this.digest = Objects.requireNonNull(valueAt("/digest", String.class)); + this.size = valueAt("/size", Long.class); + } + + public @Nullable String getMediaType() { + return this.mediaType; + } + + public String getDigest() { + return this.digest; + } + + public @Nullable Long getSize() { + return this.size; + } + + } + /** * Create a new {@link Image} instance from the specified JSON content. * @param content the JSON content From fc86c389d28d7f580435d8e662611a6ed79926d3 Mon Sep 17 00:00:00 2001 From: hojooo Date: Fri, 7 Nov 2025 14:42:55 +0900 Subject: [PATCH 8/8] Fix DocerApiTests and import route Signed-off-by: hojooo --- .../boot/buildpack/platform/build/Builder.java | 2 +- .../boot/buildpack/platform/docker/DockerApiTests.java | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index c6d77782b0cc..71d25aaaccdf 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -367,7 +367,7 @@ public void exportImageLayers(ImageReference reference, IOBiConsumer