Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -355,8 +363,16 @@ 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);
}
String digest = Builder.this.docker.image().resolveManifestDigest(reference,
this.imageFetcher.defaultPlatform);
if (StringUtils.hasText(digest)) {
ImageReference pinned = reference.withDigest(digest);
Builder.this.docker.image().exportLayers(pinned, null, exports);
}
else {
Builder.this.docker.image().exportLayers(reference, this.imageFetcher.defaultPlatform, exports);
}
}

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -239,7 +243,10 @@ public Image pull(ImageReference reference, @Nullable ImagePlatform platform,
listener.onUpdate(event);
});
}
return inspect((platform != null) ? PLATFORM_API_VERSION : API_VERSION, reference);
if (platform != null) {
return inspect(INSPECT_PLATFORM_API_VERSION, reference, platform);
}
return inspect(API_VERSION, reference);
}
finally {
listener.onFinish();
Expand Down Expand Up @@ -311,16 +318,59 @@ 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");
if (platform != null) {
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);
}
}
}

/**
* 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<String> 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
Expand All @@ -345,8 +395,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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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<LayerId> extractLayers(String @Nullable [] layers) {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,16 @@ class DockerApiTests {

private static final String PLATFORM_API_URL = "/v" + DockerApi.PLATFORM_API_VERSION;

private static final String INSPECT_PLATFORM_API_URL = "/v" + DockerApi.INSPECT_PLATFORM_API_VERSION;

public static final String PING_URL = "/_ping";

private static final String IMAGES_URL = API_URL + "/images";

private static final String PLATFORM_IMAGES_URL = PLATFORM_API_URL + "/images";

private static final String INSPECT_PLATFORM_IMAGES_URL = INSPECT_PLATFORM_API_URL + "/images";

private static final String CONTAINERS_URL = API_URL + "/containers";

private static final String PLATFORM_CONTAINERS_URL = PLATFORM_API_URL + "/containers";
Expand Down Expand Up @@ -239,9 +243,10 @@ 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(INSPECT_PLATFORM_IMAGES_URL
+ "/gcr.io/paketo-buildpacks/builder:base/json?platform=%7B%22os%22%3A%22linux%22%2C%22architecture%22%3A%22arm64%22%2C%22variant%22%3A%22v1%22%7D");
given(http().head(eq(new URI(PING_URL))))
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.41")));
.willReturn(responseWithHeaders(new BasicHeader(DockerApi.API_VERSION_HEADER_NAME, "1.49")));
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, platform, this.pullListener);
Expand Down