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 504448abccd..0657b314db0 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 @@ -391,7 +391,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/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 cb1cb499288..f84a9e20cb8 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 @@ -124,6 +124,13 @@ public class DockerApi { return this.jsonStream; } + URI buildPlatformJsonUrl(Feature feature, @Nullable 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, @Nullable Collection params) { return buildUrl(Feature.BASELINE, path, (params != null) ? params.toArray() : null); } @@ -229,9 +236,8 @@ public class DockerApi { UpdateListener listener, @Nullable 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 { @@ -313,9 +319,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, @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 = 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); @@ -359,20 +380,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, @Nullable 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"); @@ -415,7 +429,8 @@ public class DockerApi { private ContainerReference createContainer(ContainerConfig config, @Nullable 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 @@ -624,9 +639,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/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 0b5a55ae76e..a52843889bf 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 @@ -501,6 +501,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");