Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,21 @@ 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);
}
try {
ImageReference pinned = reference;
String digest = Builder.this.docker.image().resolveManifestDigest(reference,
this.imageFetcher.defaultPlatform);
if (StringUtils.hasText(digest)) {
pinned = pinned.withDigest(digest);
}
if (!pinned.equals(reference)) {
Builder.this.docker.image().exportLayers(pinned, null, exports);
}
}
catch (Exception ex) {
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