Skip to content

Commit 25a5d43

Browse files
committed
Merge branch '3.4.x' into 3.5.x
Closes gh-48098
2 parents adbeb7a + b6460ea commit 25a5d43

File tree

12 files changed

+432
-51
lines changed

12 files changed

+432
-51
lines changed

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

Lines changed: 65 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -146,16 +146,24 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio
146146
this.log.start(request);
147147
validateBindings(request.getBindings());
148148
PullPolicy pullPolicy = request.getPullPolicy();
149-
ImageFetcher imageFetcher = new ImageFetcher(this.dockerConfiguration.builderRegistryAuthentication(),
150-
pullPolicy, request.getImagePlatform());
151-
Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder());
149+
ImagePlatform platform = request.getImagePlatform();
150+
boolean specifiedPlatform = request.getImagePlatform() != null;
151+
DockerRegistryAuthentication registryAuthentication = this.dockerConfiguration.builderRegistryAuthentication();
152+
ImageFetcher imageFetcher = new ImageFetcher(registryAuthentication, pullPolicy);
153+
Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder(), platform);
152154
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
153155
request = withRunImageIfNeeded(request, builderMetadata);
154-
Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage());
156+
platform = (platform != null) ? platform : ImagePlatform.from(builderImage);
157+
Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage(), platform);
158+
if (specifiedPlatform && runImage.getPrimaryDigest() != null) {
159+
request = request.withRunImage(request.getRunImage().withDigest(runImage.getPrimaryDigest()));
160+
runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage(), platform);
161+
}
155162
assertStackIdsMatch(runImage, builderImage);
156163
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
157164
BuildpackLayersMetadata buildpackLayersMetadata = BuildpackLayersMetadata.fromImage(builderImage);
158-
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata, buildpackLayersMetadata);
165+
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, platform, builderMetadata,
166+
buildpackLayersMetadata);
159167
EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, request.getName(),
160168
builderMetadata, request.getCreator(), request.getEnv(), buildpacks);
161169
executeLifecycle(request, ephemeralBuilder);
@@ -199,9 +207,9 @@ private void assertStackIdsMatch(Image runImage, Image builderImage) {
199207
}
200208
}
201209

202-
private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata,
203-
BuildpackLayersMetadata buildpackLayersMetadata) {
204-
BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata,
210+
private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, ImagePlatform platform,
211+
BuilderMetadata builderMetadata, BuildpackLayersMetadata buildpackLayersMetadata) {
212+
BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, platform, builderMetadata,
205213
buildpackLayersMetadata);
206214
return BuildpackResolvers.resolveAll(resolverContext, request.getBuildpacks());
207215
}
@@ -263,49 +271,71 @@ private class ImageFetcher {
263271

264272
private final PullPolicy pullPolicy;
265273

266-
private ImagePlatform defaultPlatform;
267-
268-
ImageFetcher(DockerRegistryAuthentication registryAuthentication, PullPolicy pullPolicy,
269-
ImagePlatform platform) {
274+
ImageFetcher(DockerRegistryAuthentication registryAuthentication, PullPolicy pullPolicy) {
270275
this.registryAuthentication = registryAuthentication;
271276
this.pullPolicy = pullPolicy;
272-
this.defaultPlatform = platform;
273277
}
274278

275-
Image fetchImage(ImageType type, ImageReference reference) throws IOException {
279+
Image fetchImage(ImageType type, ImageReference reference, ImagePlatform platform) throws IOException {
276280
Assert.notNull(type, "'type' must not be null");
277281
Assert.notNull(reference, "'reference' must not be null");
278282
if (this.pullPolicy == PullPolicy.ALWAYS) {
279-
return checkPlatformMismatch(pullImage(reference, type), reference);
283+
return pullImageAndCheckForPlatformMismatch(type, reference, platform);
280284
}
281285
try {
282-
return checkPlatformMismatch(Builder.this.docker.image().inspect(reference), reference);
286+
Image image = Builder.this.docker.image().inspect(reference, platform);
287+
return checkPlatformMismatch(image, reference, platform);
283288
}
284289
catch (DockerEngineException ex) {
285290
if (this.pullPolicy == PullPolicy.IF_NOT_PRESENT && ex.getStatusCode() == 404) {
286-
return checkPlatformMismatch(pullImage(reference, type), reference);
291+
return pullImageAndCheckForPlatformMismatch(type, reference, platform);
292+
}
293+
throw ex;
294+
}
295+
}
296+
297+
private Image pullImageAndCheckForPlatformMismatch(ImageType type, ImageReference reference,
298+
ImagePlatform platform) throws IOException {
299+
try {
300+
Image image = pullImage(reference, type, platform);
301+
return checkPlatformMismatch(image, reference, platform);
302+
}
303+
catch (DockerEngineException ex) {
304+
// Try to throw our own exception for consistent log output. Matching
305+
// on the message is a little brittle, but it doesn't matter too much
306+
// if it fails as the original exception is still enough to stop the build
307+
if (platform != null && ex.getMessage().contains("does not provide the specified platform")) {
308+
throwAsPlatformMismatchException(type, reference, platform, ex);
287309
}
288310
throw ex;
289311
}
290312
}
291313

292-
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
314+
private void throwAsPlatformMismatchException(ImageType type, ImageReference reference, ImagePlatform platform,
315+
Throwable cause) throws IOException {
316+
try {
317+
Image image = pullImage(reference, type, null);
318+
throw new PlatformMismatchException(reference, platform, ImagePlatform.from(image), cause);
319+
}
320+
catch (DockerEngineException ex) {
321+
}
322+
}
323+
324+
private Image pullImage(ImageReference reference, ImageType imageType, ImagePlatform platform)
325+
throws IOException {
293326
TotalProgressPullListener listener = new TotalProgressPullListener(
294-
Builder.this.log.pullingImage(reference, this.defaultPlatform, imageType));
327+
Builder.this.log.pullingImage(reference, platform, imageType));
295328
String authHeader = authHeader(this.registryAuthentication, reference);
296-
Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, authHeader);
329+
Image image = Builder.this.docker.image().pull(reference, platform, listener, authHeader);
297330
Builder.this.log.pulledImage(image, imageType);
298-
if (this.defaultPlatform == null) {
299-
this.defaultPlatform = ImagePlatform.from(image);
300-
}
301331
return image;
302332
}
303333

304-
private Image checkPlatformMismatch(Image image, ImageReference imageReference) {
305-
if (this.defaultPlatform != null) {
306-
ImagePlatform imagePlatform = ImagePlatform.from(image);
307-
if (!imagePlatform.equals(this.defaultPlatform)) {
308-
throw new PlatformMismatchException(imageReference, this.defaultPlatform, imagePlatform);
334+
private Image checkPlatformMismatch(Image image, ImageReference reference, ImagePlatform requestedPlatform) {
335+
if (requestedPlatform != null) {
336+
ImagePlatform actualPlatform = ImagePlatform.from(image);
337+
if (!actualPlatform.equals(requestedPlatform)) {
338+
throw new PlatformMismatchException(reference, requestedPlatform, actualPlatform, null);
309339
}
310340
}
311341
return image;
@@ -316,9 +346,9 @@ private Image checkPlatformMismatch(Image image, ImageReference imageReference)
316346
private static final class PlatformMismatchException extends RuntimeException {
317347

318348
private PlatformMismatchException(ImageReference imageReference, ImagePlatform requestedPlatform,
319-
ImagePlatform actualPlatform) {
349+
ImagePlatform actualPlatform, Throwable cause) {
320350
super("Image platform mismatch detected. The configured platform '%s' is not supported by the image '%s'. Requested platform '%s' but got '%s'"
321-
.formatted(requestedPlatform, imageReference, requestedPlatform, actualPlatform));
351+
.formatted(requestedPlatform, imageReference, requestedPlatform, actualPlatform), cause);
322352
}
323353

324354
}
@@ -364,13 +394,16 @@ private class BuilderResolverContext implements BuildpackResolverContext {
364394

365395
private final ImageFetcher imageFetcher;
366396

397+
private final ImagePlatform platform;
398+
367399
private final BuilderMetadata builderMetadata;
368400

369401
private final BuildpackLayersMetadata buildpackLayersMetadata;
370402

371-
BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata,
403+
BuilderResolverContext(ImageFetcher imageFetcher, ImagePlatform platform, BuilderMetadata builderMetadata,
372404
BuildpackLayersMetadata buildpackLayersMetadata) {
373405
this.imageFetcher = imageFetcher;
406+
this.platform = platform;
374407
this.builderMetadata = builderMetadata;
375408
this.buildpackLayersMetadata = buildpackLayersMetadata;
376409
}
@@ -387,7 +420,7 @@ public BuildpackLayersMetadata getBuildpackLayersMetadata() {
387420

388421
@Override
389422
public Image fetchImage(ImageReference reference, ImageType imageType) throws IOException {
390-
return this.imageFetcher.fetchImage(imageType, reference);
423+
return this.imageFetcher.fetchImage(imageType, reference, this.platform);
391424
}
392425

393426
@Override

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

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public class DockerApi {
6565

6666
static final ApiVersion PLATFORM_API_VERSION = ApiVersion.of(1, 41);
6767

68+
static final ApiVersion PLATFORM_INSPECT_API_VERSION = ApiVersion.of(1, 49);
69+
6870
static final ApiVersion UNKNOWN_API_VERSION = ApiVersion.of(0, 0);
6971

7072
static final String API_VERSION_HEADER_NAME = "API-Version";
@@ -251,7 +253,7 @@ public Image pull(ImageReference reference, ImagePlatform platform,
251253
listener.onUpdate(event);
252254
});
253255
}
254-
return inspect((platform != null) ? PLATFORM_API_VERSION : API_VERSION, reference);
256+
return inspect(reference, platform);
255257
}
256258
finally {
257259
listener.onFinish();
@@ -350,17 +352,36 @@ public void remove(ImageReference reference, boolean force) throws IOException {
350352
* @throws IOException on IO error
351353
*/
352354
public Image inspect(ImageReference reference) throws IOException {
353-
return inspect(API_VERSION, reference);
355+
return inspect(reference, null);
354356
}
355357

356-
private Image inspect(ApiVersion apiVersion, ImageReference reference) throws IOException {
358+
/**
359+
* Inspect an image.
360+
* @param reference the image reference
361+
* @param platform the platform (os/architecture/variant) of the image to inspect.
362+
* Ignored on older versions of Docker.
363+
* @return the image from the local repository
364+
* @throws IOException on IO error
365+
* @since 3.4.12
366+
*/
367+
public Image inspect(ImageReference reference, ImagePlatform platform) throws IOException {
368+
// The Docker documentation is incomplete but platform parameters
369+
// are supported since 1.49 (see https://github.com/moby/moby/pull/49586)
357370
Assert.notNull(reference, "'reference' must not be null");
358-
URI imageUri = buildUrl(apiVersion, "/images/" + reference + "/json");
359-
try (Response response = http().get(imageUri)) {
371+
URI inspectUrl = inspectUrl(reference, platform);
372+
try (Response response = http().get(inspectUrl)) {
360373
return Image.of(response.getContent());
361374
}
362375
}
363376

377+
private URI inspectUrl(ImageReference reference, ImagePlatform platform) {
378+
String path = "/images/" + reference + "/json";
379+
if (platform != null && getApiVersion().supports(PLATFORM_INSPECT_API_VERSION)) {
380+
return buildUrl(PLATFORM_INSPECT_API_VERSION, path, "platform", platform.toJson());
381+
}
382+
return buildUrl(path);
383+
}
384+
364385
public void tag(ImageReference sourceReference, ImageReference targetReference) throws IOException {
365386
Assert.notNull(sourceReference, "'sourceReference' must not be null");
366387
Assert.notNull(targetReference, "'targetReference' must not be null");

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

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

2627
import com.fasterxml.jackson.databind.JsonNode;
2728

2829
import org.springframework.boot.buildpack.platform.json.MappedObject;
30+
import org.springframework.util.CollectionUtils;
2931
import org.springframework.util.StringUtils;
3032

3133
/**
@@ -51,6 +53,8 @@ public class Image extends MappedObject {
5153

5254
private final String created;
5355

56+
private final Descriptor descriptor;
57+
5458
Image(JsonNode node) {
5559
super(node, MethodHandles.lookup());
5660
this.digests = childrenAt("/RepoDigests", JsonNode::asText);
@@ -60,6 +64,9 @@ public class Image extends MappedObject {
6064
this.architecture = valueAt("/Architecture", String.class);
6165
this.variant = valueAt("/Variant", String.class);
6266
this.created = valueAt("/Created", String.class);
67+
JsonNode descriptorNode = getNode().path("Descriptor");
68+
this.descriptor = (descriptorNode.isMissingNode() || descriptorNode.isNull()) ? null
69+
: new Descriptor(descriptorNode);
6370
}
6471

6572
private List<LayerId> extractLayers(String[] layers) {
@@ -125,6 +132,34 @@ public String getCreated() {
125132
return this.created;
126133
}
127134

135+
/**
136+
* Return the descriptor for this image as reported by Docker Engine inspect.
137+
* @return the image descriptor or {@code null}
138+
*/
139+
public Descriptor getDescriptor() {
140+
return this.descriptor;
141+
}
142+
143+
/**
144+
* Return the primary digest of the image or {@code null}. Checks the
145+
* {@code Descriptor.digest} first, falling back to {@code RepoDigest}.
146+
* @return the primary digest or {@code null}
147+
* @since 3.4.12
148+
*/
149+
public String getPrimaryDigest() {
150+
if (this.descriptor != null && StringUtils.hasText(this.descriptor.getDigest())) {
151+
return this.descriptor.getDigest();
152+
}
153+
if (!CollectionUtils.isEmpty(this.digests)) {
154+
try {
155+
return ImageReference.of(this.digests.get(0)).getDigest();
156+
}
157+
catch (RuntimeException ex) {
158+
}
159+
}
160+
return null;
161+
}
162+
128163
/**
129164
* Create a new {@link Image} instance from the specified JSON content.
130165
* @param content the JSON content
@@ -135,4 +170,24 @@ public static Image of(InputStream content) throws IOException {
135170
return of(content, Image::new);
136171
}
137172

173+
/**
174+
* Descriptor details as reported in the {@code Docker inspect} response.
175+
*
176+
* @since 3.4.12
177+
*/
178+
public final class Descriptor extends MappedObject {
179+
180+
private final String digest;
181+
182+
Descriptor(JsonNode node) {
183+
super(node, MethodHandles.lookup());
184+
this.digest = Objects.requireNonNull(valueAt("/digest", String.class));
185+
}
186+
187+
public String getDigest() {
188+
return this.digest;
189+
}
190+
191+
}
192+
138193
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/type/ImagePlatform.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Objects;
2020

2121
import org.springframework.util.Assert;
22+
import org.springframework.util.StringUtils;
2223

2324
/**
2425
* A platform specification for a Docker image.
@@ -99,4 +100,24 @@ public static ImagePlatform from(Image image) {
99100
return new ImagePlatform(image.getOs(), image.getArchitecture(), image.getVariant());
100101
}
101102

103+
/**
104+
* Return a JSON-encoded representation of this platform.
105+
* @return the JSON string
106+
*/
107+
public String toJson() {
108+
StringBuilder json = new StringBuilder("{");
109+
json.append(jsonPair("os", this.os));
110+
if (StringUtils.hasText(this.architecture)) {
111+
json.append(",").append(jsonPair("architecture", this.architecture));
112+
}
113+
if (StringUtils.hasText(this.variant)) {
114+
json.append(",").append(jsonPair("variant", this.variant));
115+
}
116+
return json.append("}").toString();
117+
}
118+
119+
private String jsonPair(String name, String value) {
120+
return "\"%s\":\"%s\"".formatted(name, value);
121+
}
122+
102123
}

0 commit comments

Comments
 (0)