Skip to content

Commit dfab18c

Browse files
Add imagePlatform option for image building
An `imagePlatform` option for the Maven and Gradle image-building goal/task can be used to specify the os/architecture of any builder, run, and buildpack images that are pulled during image building. Closes gh-40944
1 parent 67bae52 commit dfab18c

File tree

45 files changed

+1373
-275
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1373
-275
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/dockerTest/java/org/springframework/boot/buildpack/platform/docker/DockerApiIntegrationTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class DockerApiIntegrationTests {
3636
@Test
3737
void pullImage() throws IOException {
3838
this.docker.image()
39-
.pull(ImageReference.of("gcr.io/paketo-buildpacks/builder:base"),
39+
.pull(ImageReference.of("gcr.io/paketo-buildpacks/builder:base"), null,
4040
new TotalProgressPullListener(new TotalProgressBar("Pulling: ")));
4141
}
4242

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
2323
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
2424
import org.springframework.boot.buildpack.platform.docker.type.Image;
25+
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
2526
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
2627
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
2728

@@ -43,8 +44,17 @@ public void start(BuildRequest request) {
4344
}
4445

4546
@Override
46-
public Consumer<TotalProgressEvent> pullingImage(ImageReference imageReference, ImageType imageType) {
47-
return getProgressConsumer(String.format(" > Pulling %s '%s'", imageType.getDescription(), imageReference));
47+
public Consumer<TotalProgressEvent> pullingImage(ImageReference imageReference, ImagePlatform platform,
48+
ImageType imageType) {
49+
String message;
50+
if (platform != null) {
51+
message = String.format(" > Pulling %s '%s' for platform '%s'", imageType.getDescription(), imageReference,
52+
platform);
53+
}
54+
else {
55+
message = String.format(" > Pulling %s '%s'", imageType.getDescription(), imageReference);
56+
}
57+
return getProgressConsumer(message);
4858
}
4959

5060
@Override

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2023 the original author or authors.
2+
* Copyright 2012-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.util.Arrays;
2020
import java.util.stream.IntStream;
2121

22+
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
2223
import org.springframework.util.StringUtils;
2324

2425
/**

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
2323
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
2424
import org.springframework.boot.buildpack.platform.docker.type.Image;
25+
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
2526
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
2627
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
2728

@@ -46,10 +47,12 @@ public interface BuildLog {
4647
/**
4748
* Log that an image is being pulled.
4849
* @param imageReference the image reference
50+
* @param platform the platform of the image
4951
* @param imageType the image type
5052
* @return a consumer for progress update events
5153
*/
52-
Consumer<TotalProgressEvent> pullingImage(ImageReference imageReference, ImageType imageType);
54+
Consumer<TotalProgressEvent> pullingImage(ImageReference imageReference, ImagePlatform platform,
55+
ImageType imageType);
5356

5457
/**
5558
* Log that an image has been pulled.

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

Lines changed: 117 additions & 84 deletions
Large diffs are not rendered by default.

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

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
3030
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
3131
import org.springframework.boot.buildpack.platform.docker.type.Image;
32+
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
3233
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
3334
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
3435
import org.springframework.boot.buildpack.platform.io.TarArchive;
@@ -99,7 +100,8 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio
99100
this.log.start(request);
100101
String domain = request.getBuilder().getDomain();
101102
PullPolicy pullPolicy = request.getPullPolicy();
102-
ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy);
103+
ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy,
104+
request.getImagePlatform());
103105
Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder());
104106
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
105107
request = withRunImageIfNeeded(request, builderMetadata);
@@ -208,10 +210,13 @@ private class ImageFetcher {
208210

209211
private final PullPolicy pullPolicy;
210212

211-
ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy) {
213+
private ImagePlatform defaultPlatform;
214+
215+
ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy, ImagePlatform platform) {
212216
this.domain = domain;
213217
this.authHeader = authHeader;
214218
this.pullPolicy = pullPolicy;
219+
this.defaultPlatform = platform;
215220
}
216221

217222
Image fetchImage(ImageType type, ImageReference reference) throws IOException {
@@ -236,9 +241,12 @@ Image fetchImage(ImageType type, ImageReference reference) throws IOException {
236241

237242
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
238243
TotalProgressPullListener listener = new TotalProgressPullListener(
239-
Builder.this.log.pullingImage(reference, imageType));
240-
Image image = Builder.this.docker.image().pull(reference, listener, this.authHeader);
244+
Builder.this.log.pullingImage(reference, this.defaultPlatform, imageType));
245+
Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, this.authHeader);
241246
Builder.this.log.pulledImage(image, imageType);
247+
if (this.defaultPlatform == null) {
248+
this.defaultPlatform = ImagePlatform.from(image);
249+
}
242250
return image;
243251
}
244252

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.springframework.boot.buildpack.platform.docker.DockerApi;
3030
import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent;
3131
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
32+
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
3233
import org.springframework.boot.buildpack.platform.docker.type.Binding;
3334
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
3435
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
@@ -363,15 +364,16 @@ private void run(Phase phase) throws IOException {
363364

364365
private ContainerReference createContainer(ContainerConfig config, boolean requiresAppUpload) throws IOException {
365366
if (!requiresAppUpload || this.applicationVolumePopulated) {
366-
return this.docker.container().create(config);
367+
return this.docker.container().create(config, this.request.getImagePlatform());
367368
}
368369
try {
369370
if (this.application.getBind() != null) {
370371
Files.createDirectories(Path.of(this.application.getBind().getSource()));
371372
}
372373
TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner());
373374
return this.docker.container()
374-
.create(config, ContainerContent.of(applicationContent, this.applicationDirectory));
375+
.create(config, this.request.getImagePlatform(),
376+
ContainerContent.of(applicationContent, this.applicationDirectory));
375377
}
376378
finally {
377379
this.applicationVolumePopulated = true;

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

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,20 @@
2828
import java.util.List;
2929
import java.util.Objects;
3030

31+
import org.apache.hc.core5.http.Header;
3132
import org.apache.hc.core5.net.URIBuilder;
3233

3334
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration;
3435
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport;
3536
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response;
37+
import org.springframework.boot.buildpack.platform.docker.type.ApiVersion;
3638
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
3739
import org.springframework.boot.buildpack.platform.docker.type.ContainerContent;
3840
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
3941
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
4042
import org.springframework.boot.buildpack.platform.docker.type.Image;
4143
import org.springframework.boot.buildpack.platform.docker.type.ImageArchive;
44+
import org.springframework.boot.buildpack.platform.docker.type.ImagePlatform;
4245
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
4346
import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
4447
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
@@ -61,7 +64,9 @@ public class DockerApi {
6164

6265
private static final List<String> FORCE_PARAMS = Collections.unmodifiableList(Arrays.asList("force", "1"));
6366

64-
static final String API_VERSION = "v1.24";
67+
static final ApiVersion MINIMUM_API_VERSION = ApiVersion.parse("1.24");
68+
69+
static final String API_VERSION_HEADER_NAME = "API-Version";
6570

6671
private final HttpTransport http;
6772

@@ -73,6 +78,10 @@ public class DockerApi {
7378

7479
private final VolumeApi volume;
7580

81+
private final SystemApi system;
82+
83+
private ApiVersion apiVersion = null;
84+
7685
/**
7786
* Create a new {@link DockerApi} instance.
7887
*/
@@ -100,6 +109,7 @@ public DockerApi(DockerHostConfiguration dockerHost) {
100109
this.image = new ImageApi();
101110
this.container = new ContainerApi();
102111
this.volume = new VolumeApi();
112+
this.system = new SystemApi();
103113
}
104114

105115
private HttpTransport http() {
@@ -116,7 +126,10 @@ private URI buildUrl(String path, Collection<?> params) {
116126

117127
private URI buildUrl(String path, Object... params) {
118128
try {
119-
URIBuilder builder = new URIBuilder("/" + API_VERSION + path);
129+
if (this.apiVersion == null) {
130+
this.apiVersion = this.system.getApiVersion();
131+
}
132+
URIBuilder builder = new URIBuilder("/v" + this.apiVersion + path);
120133
int param = 0;
121134
while (param < params.length) {
122135
builder.addParameter(Objects.toString(params[param++]), Objects.toString(params[param++]));
@@ -128,6 +141,13 @@ private URI buildUrl(String path, Object... params) {
128141
}
129142
}
130143

144+
private void verifyApiVersionForPlatform() {
145+
ApiVersion minimumPlatformApiVersion = ApiVersion.of(1, 41);
146+
Assert.isTrue(this.apiVersion.supports(minimumPlatformApiVersion),
147+
"Docker API version must be at least " + minimumPlatformApiVersion
148+
+ " to support the 'imagePlatform' option, but current API version is " + this.apiVersion);
149+
}
150+
131151
/**
132152
* Return the Docker API for image operations.
133153
* @return the image API
@@ -148,6 +168,10 @@ public VolumeApi volume() {
148168
return this.volume;
149169
}
150170

171+
SystemApi system() {
172+
return this.system;
173+
}
174+
151175
/**
152176
* Docker API for image operations.
153177
*/
@@ -159,27 +183,37 @@ public class ImageApi {
159183
/**
160184
* Pull an image from a registry.
161185
* @param reference the image reference to pull
186+
* @param platform the platform (os/architecture/variant) of the image to pull
162187
* @param listener a pull listener to receive update events
163188
* @return the {@link ImageApi pulled image} instance
164189
* @throws IOException on IO error
165190
*/
166-
public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent> listener) throws IOException {
167-
return pull(reference, listener, null);
191+
public Image pull(ImageReference reference, ImagePlatform platform,
192+
UpdateListener<PullImageUpdateEvent> listener) throws IOException {
193+
return pull(reference, platform, listener, null);
168194
}
169195

170196
/**
171197
* Pull an image from a registry.
172198
* @param reference the image reference to pull
199+
* @param platform the platform (os/architecture/variant) of the image to pull
173200
* @param listener a pull listener to receive update events
174201
* @param registryAuth registry authentication credentials
175202
* @return the {@link ImageApi pulled image} instance
176203
* @throws IOException on IO error
177204
*/
178-
public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent> listener, String registryAuth)
179-
throws IOException {
205+
public Image pull(ImageReference reference, ImagePlatform platform,
206+
UpdateListener<PullImageUpdateEvent> listener, String registryAuth) throws IOException {
180207
Assert.notNull(reference, "Reference must not be null");
181208
Assert.notNull(listener, "Listener must not be null");
182-
URI createUri = buildUrl("/images/create", "fromImage", reference);
209+
URI createUri;
210+
if (platform != null) {
211+
createUri = buildUrl("/images/create", "fromImage", reference, "platform", platform);
212+
verifyApiVersionForPlatform();
213+
}
214+
else {
215+
createUri = buildUrl("/images/create", "fromImage", reference);
216+
}
183217
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
184218
listener.onStart();
185219
try {
@@ -348,22 +382,32 @@ public class ContainerApi {
348382
/**
349383
* Create a new container a {@link ContainerConfig}.
350384
* @param config the container config
385+
* @param platform the platform (os/architecture/variant) of the image the
386+
* container should be created from
351387
* @param contents additional contents to include
352388
* @return a {@link ContainerReference} for the newly created container
353389
* @throws IOException on IO error
354390
*/
355-
public ContainerReference create(ContainerConfig config, ContainerContent... contents) throws IOException {
391+
public ContainerReference create(ContainerConfig config, ImagePlatform platform, ContainerContent... contents)
392+
throws IOException {
356393
Assert.notNull(config, "Config must not be null");
357394
Assert.noNullElements(contents, "Contents must not contain null elements");
358-
ContainerReference containerReference = createContainer(config);
395+
ContainerReference containerReference = createContainer(config, platform);
359396
for (ContainerContent content : contents) {
360397
uploadContainerContent(containerReference, content);
361398
}
362399
return containerReference;
363400
}
364401

365-
private ContainerReference createContainer(ContainerConfig config) throws IOException {
366-
URI createUri = buildUrl("/containers/create");
402+
private ContainerReference createContainer(ContainerConfig config, ImagePlatform platform) throws IOException {
403+
URI createUri;
404+
if (platform != null) {
405+
createUri = buildUrl("/containers/create", "platform", platform);
406+
verifyApiVersionForPlatform();
407+
}
408+
else {
409+
createUri = buildUrl("/containers/create");
410+
}
367411
try (Response response = http().post(createUri, "application/json", config::writeTo)) {
368412
return ContainerReference
369413
.of(SharedObjectMapper.get().readTree(response.getContent()).at("/Id").asText());
@@ -460,6 +504,39 @@ public void delete(VolumeName name, boolean force) throws IOException {
460504

461505
}
462506

507+
/**
508+
* Docker API for system operations.
509+
*/
510+
class SystemApi {
511+
512+
SystemApi() {
513+
}
514+
515+
/**
516+
* Get the API version supported by the Docker daemon.
517+
* @return the Docker daemon API version
518+
*/
519+
ApiVersion getApiVersion() {
520+
try {
521+
URI uri = new URIBuilder("/_ping").build();
522+
try (Response response = http().head(uri)) {
523+
Header apiVersionHeader = response.getHeader(API_VERSION_HEADER_NAME);
524+
if (apiVersionHeader != null) {
525+
return ApiVersion.parse(apiVersionHeader.getValue());
526+
}
527+
}
528+
catch (Exception ex) {
529+
// fall through to return default value
530+
}
531+
return MINIMUM_API_VERSION;
532+
}
533+
catch (URISyntaxException ex) {
534+
throw new IllegalStateException(ex);
535+
}
536+
}
537+
538+
}
539+
463540
/**
464541
* {@link UpdateListener} used to capture the image digest.
465542
*/

0 commit comments

Comments
 (0)