Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ public Image fetchImage(ImageReference reference, ImageType imageType) throws IO
@Override
public void exportImageLayers(ImageReference reference, IOBiConsumer<String, TarArchive> exports)
throws IOException {
Builder.this.docker.image().exportLayers(reference, exports);
Builder.this.docker.image().exportLayers(reference, this.imageFetcher.defaultPlatform, exports);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -311,9 +313,24 @@ public void load(ImageArchive archive, UpdateListener<LoadImageUpdateEvent> list
*/
public void exportLayers(ImageReference reference, IOBiConsumer<String, TarArchive> 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<String, TarArchive> 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);
Expand Down Expand Up @@ -549,6 +566,10 @@ public void onUpdate(ProgressUpdateEvent event) {
}
}

private @Nullable String getDigest() {
return this.digest;
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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");
Expand All @@ -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"));
Expand Down Expand Up @@ -447,6 +450,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<String, String> 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");
Expand Down