Skip to content

Commit 3bd8188

Browse files
committed
Fix Selected Index Digest to Manifest Digest
Signed-off-by: hojooo <ghwn5833@gmail.com>
1 parent 772add5 commit 3bd8188

File tree

3 files changed

+104
-25
lines changed

3 files changed

+104
-25
lines changed

buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,22 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio
105105
Assert.notNull(request, "'request' must not be null");
106106
this.log.start(request);
107107
validateBindings(request.getBindings());
108-
PullPolicy pullPolicy = request.getPullPolicy();
109-
ImageFetcher imageFetcher = new ImageFetcher(this.dockerConfiguration.builderRegistryAuthentication(),
110-
pullPolicy, request.getImagePlatform());
108+
PullPolicy pullPolicy = request.getPullPolicy();
109+
ImagePlatform requestedPlatform = request.getImagePlatform();
110+
ImageFetcher imageFetcher = new ImageFetcher(this.dockerConfiguration.builderRegistryAuthentication(),
111+
pullPolicy, requestedPlatform);
111112
Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder());
112113
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
113114
request = withRunImageIfNeeded(request, builderMetadata);
114-
Assert.state(request.getRunImage() != null, "'request.getRunImage()' must not be null");
115-
Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage());
115+
ImageReference imageReference = request.getRunImage();
116+
Assert.state(imageReference != null, "'imageReference' must not be null");
117+
Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, imageReference);
118+
String digest = this.docker.image().resolveManifestDigest(imageReference, requestedPlatform);
119+
if (StringUtils.hasText(digest)) {
120+
imageReference = imageReference.withDigest(digest);
121+
runImage = imageFetcher.fetchImage(ImageType.RUNNER, imageReference);
122+
}
123+
request = request.withRunImage(imageReference);
116124
assertStackIdsMatch(runImage, builderImage);
117125
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
118126
BuildpackLayersMetadata buildpackLayersMetadata = BuildpackLayersMetadata.fromImage(builderImage);
@@ -355,8 +363,21 @@ public Image fetchImage(ImageReference reference, ImageType imageType) throws IO
355363
@Override
356364
public void exportImageLayers(ImageReference reference, IOBiConsumer<String, TarArchive> exports)
357365
throws IOException {
358-
Builder.this.docker.image().exportLayers(reference, this.imageFetcher.defaultPlatform, exports);
359-
}
366+
try {
367+
ImageReference pinned = reference;
368+
String digest = Builder.this.docker.image().resolveManifestDigest(reference,
369+
this.imageFetcher.defaultPlatform);
370+
if (org.springframework.util.StringUtils.hasText(digest)) {
371+
pinned = pinned.withDigest(digest);
372+
}
373+
if (!pinned.equals(reference)) {
374+
Builder.this.docker.image().exportLayers(pinned, null, exports);
375+
}
376+
}
377+
catch (Exception ex) {
378+
Builder.this.docker.image().exportLayers(reference, this.imageFetcher.defaultPlatform, exports);
379+
}
380+
}
360381

361382
}
362383

buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -243,16 +243,9 @@ public Image pull(ImageReference reference, @Nullable ImagePlatform platform,
243243
listener.onUpdate(event);
244244
});
245245
}
246-
if (platform != null) {
247-
if (getApiVersion().supports(INSPECT_PLATFORM_API_VERSION)) {
248-
return inspect(INSPECT_PLATFORM_API_VERSION, reference, platform);
249-
}
250-
String digest = digestCapture.getDigest();
251-
if (digest != null) {
252-
ImageReference digestRef = reference.withDigest(digest);
253-
return inspect(PLATFORM_API_VERSION, digestRef);
254-
}
255-
}
246+
if (platform != null) {
247+
return inspect(INSPECT_PLATFORM_API_VERSION, reference, platform);
248+
}
256249
return inspect(API_VERSION, reference);
257250
}
258251
finally {
@@ -342,10 +335,8 @@ public void exportLayers(ImageReference reference, @Nullable ImagePlatform platf
342335
Assert.notNull(exports, "'exports' must not be null");
343336
URI uri = buildUrl("/images/" + reference + "/get");
344337
if (platform != null) {
345-
if (getApiVersion().supports(EXPORT_PLATFORM_API_VERSION)) {
346-
uri = buildUrl(EXPORT_PLATFORM_API_VERSION, "/images/" + reference + "/get", "platform",
347-
platform.toJson());
348-
}
338+
uri = buildUrl(EXPORT_PLATFORM_API_VERSION, "/images/" + reference + "/get", "platform",
339+
platform.toJson());
349340
}
350341
try (Response response = http().get(uri)) {
351342
try (ExportedImageTar exportedImageTar = new ExportedImageTar(reference, response.getContent())) {
@@ -354,6 +345,32 @@ public void exportLayers(ImageReference reference, @Nullable ImagePlatform platf
354345
}
355346
}
356347

348+
/**
349+
* Resolve an image manifest digest via Docker inspect.
350+
* If {@code platform} is provided, performs a platform-aware inspect.
351+
* Preference order: {@code Descriptor.digest} then first {@code RepoDigest}.
352+
* Returns an empty string if no digest can be determined.
353+
* @param reference image reference
354+
* @param platform desired platform
355+
* @return resolved digest or empty string
356+
* @throws IOException on IO error
357+
*/
358+
public String resolveManifestDigest(ImageReference reference, @Nullable ImagePlatform platform)
359+
throws IOException {
360+
Assert.notNull(reference, "'reference' must not be null");
361+
Image image = inspect(API_VERSION, reference);
362+
if (platform != null) {
363+
image = inspect(INSPECT_PLATFORM_API_VERSION, reference, platform);
364+
}
365+
Image.Descriptor descriptor = image.getDescriptor();
366+
if (descriptor != null && StringUtils.hasText(descriptor.getDigest())) {
367+
return descriptor.getDigest();
368+
}
369+
List<String> repoDigests = image.getDigests();
370+
String digest = repoDigests.isEmpty() ? "" : repoDigests.get(0);
371+
return digest.substring(digest.indexOf('@') + 1);
372+
}
373+
357374
/**
358375
* Remove a specific image.
359376
* @param reference the reference the remove
@@ -589,10 +606,6 @@ public void onUpdate(ProgressUpdateEvent event) {
589606
}
590607
}
591608

592-
private @Nullable String getDigest() {
593-
return this.digest;
594-
}
595-
596609
}
597610

598611
/**

buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/Image.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Arrays;
2323
import java.util.Collections;
2424
import java.util.List;
25+
import java.util.Objects;
2526

2627
import org.jspecify.annotations.Nullable;
2728
import tools.jackson.databind.JsonNode;
@@ -52,6 +53,8 @@ public class Image extends MappedObject {
5253

5354
private final @Nullable String created;
5455

56+
private final @Nullable Descriptor descriptor;
57+
5558
Image(JsonNode node) {
5659
super(node, MethodHandles.lookup());
5760
this.digests = childrenAt("/RepoDigests", JsonNode::asString);
@@ -61,6 +64,8 @@ public class Image extends MappedObject {
6164
this.architecture = valueAt("/Architecture", String.class);
6265
this.variant = valueAt("/Variant", String.class);
6366
this.created = valueAt("/Created", String.class);
67+
JsonNode descriptorNode = getNode().path("Descriptor");
68+
this.descriptor = (descriptorNode.isMissingNode() || descriptorNode.isNull()) ? null : new Descriptor(descriptorNode);
6469
}
6570

6671
private List<LayerId> extractLayers(String @Nullable [] layers) {
@@ -126,6 +131,46 @@ public String getOs() {
126131
return this.created;
127132
}
128133

134+
/**
135+
* Return the descriptor for this image as reported by Docker Engine inspect.
136+
* @return the image descriptor or {@code null}
137+
*/
138+
public @Nullable Descriptor getDescriptor() {
139+
return this.descriptor;
140+
}
141+
142+
/**
143+
* Descriptor details as reported by the Docker Engine inspect response.
144+
*/
145+
public static final class Descriptor extends MappedObject {
146+
147+
private final @Nullable String mediaType;
148+
149+
private final String digest;
150+
151+
private final @Nullable Long size;
152+
153+
Descriptor(JsonNode node) {
154+
super(node, MethodHandles.lookup());
155+
this.mediaType = valueAt("/mediaType", String.class);
156+
this.digest = Objects.requireNonNull(valueAt("/digest", String.class));
157+
this.size = valueAt("/size", Long.class);
158+
}
159+
160+
public @Nullable String getMediaType() {
161+
return this.mediaType;
162+
}
163+
164+
public String getDigest() {
165+
return this.digest;
166+
}
167+
168+
public @Nullable Long getSize() {
169+
return this.size;
170+
}
171+
172+
}
173+
129174
/**
130175
* Create a new {@link Image} instance from the specified JSON content.
131176
* @param content the JSON content

0 commit comments

Comments
 (0)