From 53bda71c3e3c611175f75ac2666c191affbb6048 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Thu, 13 Nov 2025 15:19:26 -0800 Subject: [PATCH] Use platform when exporting buildpack images Update `DockerApi` and `Builder` to support export of images with a specified platform. This prevents 'NotFound: content digest sha256:' errors when building an amd64 image on an arm64 machine. Fixes gh-46665 --- .../buildpack/platform/build/Builder.java | 2 +- .../buildpack/platform/docker/DockerApi.java | 51 +++++++++++----- .../platform/docker/DockerApiTests.java | 59 +++++++++++++++++++ 3 files changed, 95 insertions(+), 17 deletions(-) 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 7c1e1b13ee9..2cd7f69b223 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 @@ -358,7 +358,7 @@ public class Builder { @Override public void exportImageLayers(ImageReference reference, IOBiConsumer exports) throws IOException { - Builder.this.docker.image().exportLayers(reference, exports); + Builder.this.docker.image().exportLayers(reference, this.platform, exports); } } 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 bec2416432e..00a3c8a3400 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 @@ -122,6 +122,13 @@ public class DockerApi { return this.jsonStream; } + URI buildPlatformJsonUrl(Feature feature, ImagePlatform platform, String path) { + if (platform != null && getApiVersion().supports(feature.minimumVersion())) { + return buildUrl(feature, path, "platform", platform.toJson()); + } + return buildUrl(path); + } + private URI buildUrl(String path, Collection params) { return buildUrl(Feature.BASELINE, path, (params != null) ? params.toArray() : null); } @@ -227,9 +234,8 @@ public class DockerApi { UpdateListener listener, String registryAuth) throws IOException { Assert.notNull(reference, "Reference must not be null"); Assert.notNull(listener, "Listener must not be null"); - URI createUri = (platform != null) - ? buildUrl(Feature.PLATFORM, "/images/create", "fromImage", reference, "platform", platform) - : buildUrl("/images/create", "fromImage", reference); + URI createUri = (platform != null) ? buildUrl(Feature.PLATFORM_IMAGE_PULL, "/images/create", "fromImage", + reference, "platform", platform) : buildUrl("/images/create", "fromImage", reference); DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener(); listener.onStart(); try { @@ -336,9 +342,24 @@ public class DockerApi { */ 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. + * Ignored on older versions of Docker. + * @param exports a consumer to receive the layers (contents can only be accessed + * during the callback) + * @throws IOException on IO error + * @since 3.4.12 + */ + public void exportLayers(ImageReference reference, 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 = buildPlatformJsonUrl(Feature.PLATFORM_IMAGE_EXPORT, platform, "/images/" + reference + "/get"); try (Response response = http().get(uri)) { try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, response.getContent())) { exportedImageTar.exportLayers(exports); @@ -382,20 +403,13 @@ public class DockerApi { // The Docker documentation is incomplete but platform parameters // are supported since 1.49 (see https://github.com/moby/moby/pull/49586) Assert.notNull(reference, "Reference must not be null"); - URI inspectUrl = inspectUrl(reference, platform); + URI inspectUrl = buildPlatformJsonUrl(Feature.PLATFORM_IMAGE_INSPECT, platform, + "/images/" + reference + "/json"); try (Response response = http().get(inspectUrl)) { return Image.of(response.getContent()); } } - private URI inspectUrl(ImageReference reference, ImagePlatform platform) { - String path = "/images/" + reference + "/json"; - if (platform != null && getApiVersion().supports(Feature.PLATFORM_INSPECT.minimumVersion())) { - return buildUrl(Feature.PLATFORM_INSPECT, path, "platform", platform.toJson()); - } - return buildUrl(path); - } - public void tag(ImageReference sourceReference, ImageReference targetReference) throws IOException { Assert.notNull(sourceReference, "SourceReference must not be null"); Assert.notNull(targetReference, "TargetReference must not be null"); @@ -437,7 +451,8 @@ public class DockerApi { } private ContainerReference createContainer(ContainerConfig config, ImagePlatform platform) throws IOException { - URI createUri = (platform != null) ? buildUrl(Feature.PLATFORM, "/containers/create", "platform", platform) + URI createUri = (platform != null) + ? buildUrl(Feature.PLATFORM_CONTAINER_CREATE, "/containers/create", "platform", platform) : buildUrl("/containers/create"); try (Response response = http().post(createUri, "application/json", config::writeTo)) { return ContainerReference @@ -639,9 +654,13 @@ public class DockerApi { BASELINE(ApiVersion.of(1, 24)), - PLATFORM(ApiVersion.of(1, 41)), + PLATFORM_IMAGE_PULL(ApiVersion.of(1, 41)), - PLATFORM_INSPECT(ApiVersion.of(1, 49)); + PLATFORM_CONTAINER_CREATE(ApiVersion.of(1, 41)), + + PLATFORM_IMAGE_INSPECT(ApiVersion.of(1, 49)), + + PLATFORM_IMAGE_EXPORT(ApiVersion.of(1, 48)); private final ApiVersion minimumVersion; 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 443cbd77d61..661b66776e6 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 @@ -505,6 +505,65 @@ class DockerApiTests { .containsExactly("/cnb/stack.toml"); } + @Test + void exportLayersExportsLayerTarsWithPlatformWhenSupportedVersion() throws Exception { + setVersion("1.48"); + ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base"); + URI exportUri = new URI("/v1.48/images/docker.io/paketobuildpacks/builder:base/get?platform=" + + ENCODED_LINUX_ARM64_PLATFORM_JSON); + given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export.tar")); + MultiValueMap contents = new LinkedMultiValueMap<>(); + this.api.exportLayers(reference, LINUX_ARM64_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 exportLayersExportsLayerTarsWithPlatformWhenOldVersionInspectImage() throws Exception { + setVersion("1.47"); + ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base"); + URI exportUri = new URI("/v1.47/images/docker.io/paketobuildpacks/builder:base/get"); + given(DockerApiTests.this.http.get(exportUri)).willReturn(responseOf("export.tar")); + MultiValueMap contents = new LinkedMultiValueMap<>(); + this.api.exportLayers(reference, LINUX_ARM64_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 exportLayersWithSymlinksExportsLayerTars() throws Exception { ImageReference reference = ImageReference.of("docker.io/paketobuildpacks/builder:base");