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..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 @@ -68,6 +68,10 @@ public class DockerApi { static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41); + static final ApiVersion EXPORT_PLATFORM_API_VERSION = ApiVersion.of(1, 48); + + static final ApiVersion INSPECT_PLATFORM_API_VERSION = ApiVersion.of(1, 49); + static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0); static final String API_VERSION_HEADER_NAME = "API-Version"; @@ -239,7 +243,17 @@ public Image pull(ImageReference reference, @Nullable ImagePlatform platform, listener.onUpdate(event); }); } - return inspect((platform != null) ? PLATFORM_API_VERSION : API_VERSION, reference); + 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); + } + } + return inspect(API_VERSION, reference); } finally { listener.onFinish(); @@ -311,9 +325,28 @@ 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"); + if (platform != null) { + if (getApiVersion().supports(EXPORT_PLATFORM_API_VERSION)) { + 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())) { exportedImageTar.exportLayers(exports); @@ -345,8 +378,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.toJson()) + : buildUrl(apiVersion, "/images/" + reference + "/json"); try (Response response = http().get(imageUri)) { return Image.of(response.getContent()); } @@ -549,6 +589,10 @@ 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/ImagePlatform.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java index 75bfbfbcaafb..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 @@ -101,4 +101,22 @@ public static ImagePlatform from(Image image) { return new ImagePlatform(image.getOs(), image.getArchitecture(), image.getVariant()); } + /** + * 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 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("\""); + } + if (this.variant != null && !this.variant.isEmpty()) { + json.append(",\"variant\":\"").append(this.variant).append("\""); + } + json.append("}"); + return json.toString(); + } } 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..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 @@ -239,7 +239,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"));