Skip to content

Commit f54f784

Browse files
Add buildpack option for image building
This commit adds configuration to the Maven and Gradle plugins to allow a list of buildpacks to be provided to the image building goal and task. Fixes gh-21722
1 parent d8fe9de commit f54f784

File tree

74 files changed

+3892
-208
lines changed

Some content is hidden

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

74 files changed

+3892
-208
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ dependencies {
1313
api("org.apache.commons:commons-compress:1.19")
1414
api("org.apache.httpcomponents:httpclient")
1515
api("org.springframework:spring-core")
16+
api("org.tomlj:tomlj:1.0.0")
1617

1718
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
1819
testImplementation("com.jayway.jsonpath:json-path")

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

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2020 the original author or authors.
2+
* Copyright 2012-2021 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.
@@ -17,8 +17,10 @@
1717
package org.springframework.boot.buildpack.platform.build;
1818

1919
import java.io.File;
20+
import java.util.Arrays;
2021
import java.util.Collections;
2122
import java.util.LinkedHashMap;
23+
import java.util.List;
2224
import java.util.Map;
2325
import java.util.function.Function;
2426

@@ -61,6 +63,8 @@ public class BuildRequest {
6163

6264
private final boolean publish;
6365

66+
private final List<BuildpackReference> buildpacks;
67+
6468
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
6569
Assert.notNull(name, "Name must not be null");
6670
Assert.notNull(applicationContent, "ApplicationContent must not be null");
@@ -74,11 +78,12 @@ public class BuildRequest {
7478
this.pullPolicy = PullPolicy.ALWAYS;
7579
this.publish = false;
7680
this.creator = Creator.withVersion("");
81+
this.buildpacks = Collections.emptyList();
7782
}
7883

7984
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
8085
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
81-
boolean verboseLogging, PullPolicy pullPolicy, boolean publish) {
86+
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks) {
8287
this.name = name;
8388
this.applicationContent = applicationContent;
8489
this.builder = builder;
@@ -89,6 +94,7 @@ public class BuildRequest {
8994
this.verboseLogging = verboseLogging;
9095
this.pullPolicy = pullPolicy;
9196
this.publish = publish;
97+
this.buildpacks = buildpacks;
9298
}
9399

94100
/**
@@ -99,7 +105,8 @@ public class BuildRequest {
99105
public BuildRequest withBuilder(ImageReference builder) {
100106
Assert.notNull(builder, "Builder must not be null");
101107
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
102-
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
108+
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
109+
this.buildpacks);
103110
}
104111

105112
/**
@@ -109,7 +116,8 @@ public BuildRequest withBuilder(ImageReference builder) {
109116
*/
110117
public BuildRequest withRunImage(ImageReference runImageName) {
111118
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
112-
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
119+
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
120+
this.buildpacks);
113121
}
114122

115123
/**
@@ -120,7 +128,7 @@ public BuildRequest withRunImage(ImageReference runImageName) {
120128
public BuildRequest withCreator(Creator creator) {
121129
Assert.notNull(creator, "Creator must not be null");
122130
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
123-
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
131+
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks);
124132
}
125133

126134
/**
@@ -135,7 +143,8 @@ public BuildRequest withEnv(String name, String value) {
135143
Map<String, String> env = new LinkedHashMap<>(this.env);
136144
env.put(name, value);
137145
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
138-
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
146+
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
147+
this.buildpacks);
139148
}
140149

141150
/**
@@ -149,7 +158,7 @@ public BuildRequest withEnv(Map<String, String> env) {
149158
updatedEnv.putAll(env);
150159
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
151160
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy,
152-
this.publish);
161+
this.publish, this.buildpacks);
153162
}
154163

155164
/**
@@ -159,7 +168,7 @@ public BuildRequest withEnv(Map<String, String> env) {
159168
*/
160169
public BuildRequest withCleanCache(boolean cleanCache) {
161170
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
162-
cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
171+
cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks);
163172
}
164173

165174
/**
@@ -169,7 +178,7 @@ public BuildRequest withCleanCache(boolean cleanCache) {
169178
*/
170179
public BuildRequest withVerboseLogging(boolean verboseLogging) {
171180
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
172-
this.cleanCache, verboseLogging, this.pullPolicy, this.publish);
181+
this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks);
173182
}
174183

175184
/**
@@ -179,7 +188,7 @@ public BuildRequest withVerboseLogging(boolean verboseLogging) {
179188
*/
180189
public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
181190
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
182-
this.cleanCache, this.verboseLogging, pullPolicy, this.publish);
191+
this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks);
183192
}
184193

185194
/**
@@ -189,7 +198,28 @@ public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
189198
*/
190199
public BuildRequest withPublish(boolean publish) {
191200
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
192-
this.cleanCache, this.verboseLogging, this.pullPolicy, publish);
201+
this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks);
202+
}
203+
204+
/**
205+
* Return a new {@link BuildRequest} with an updated buildpacks setting.
206+
* @param buildpacks a collection of buildpacks to use when building the image
207+
* @return an updated build request
208+
*/
209+
public BuildRequest withBuildpacks(BuildpackReference... buildpacks) {
210+
Assert.notEmpty(buildpacks, "Buildpacks must not be empty");
211+
return withBuildpacks(Arrays.asList(buildpacks));
212+
}
213+
214+
/**
215+
* Return a new {@link BuildRequest} with an updated buildpacks setting.
216+
* @param buildpacks a collection of buildpacks to use when building the image
217+
* @return an updated build request
218+
*/
219+
public BuildRequest withBuildpacks(List<BuildpackReference> buildpacks) {
220+
Assert.notNull(buildpacks, "Buildpacks must not be null");
221+
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
222+
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks);
193223
}
194224

195225
/**
@@ -275,6 +305,14 @@ public PullPolicy getPullPolicy() {
275305
return this.pullPolicy;
276306
}
277307

308+
/**
309+
* Return the collection of buildpacks to use when building the image, if provided.
310+
* @return the collection of buildpacks
311+
*/
312+
public List<BuildpackReference> getBuildpacks() {
313+
return this.buildpacks;
314+
}
315+
278316
/**
279317
* Factory method to create a new {@link BuildRequest} from a JAR file.
280318
* @param jarFile the source jar file

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

Lines changed: 105 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.boot.buildpack.platform.build;
1818

1919
import java.io.IOException;
20+
import java.util.List;
2021
import java.util.function.Consumer;
2122

2223
import org.springframework.boot.buildpack.platform.build.BuilderMetadata.Stack;
@@ -29,6 +30,8 @@
2930
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
3031
import org.springframework.boot.buildpack.platform.docker.type.Image;
3132
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
33+
import org.springframework.boot.buildpack.platform.io.IOBiConsumer;
34+
import org.springframework.boot.buildpack.platform.io.TarArchive;
3235
import org.springframework.util.Assert;
3336
import org.springframework.util.StringUtils;
3437

@@ -92,34 +95,35 @@ public Builder(BuildLog log, DockerConfiguration dockerConfiguration) {
9295
public void build(BuildRequest request) throws DockerEngineException, IOException {
9396
Assert.notNull(request, "Request must not be null");
9497
this.log.start(request);
95-
Image builderImage = getImage(request, ImageType.BUILDER);
98+
String domain = request.getBuilder().getDomain();
99+
PullPolicy pullPolicy = request.getPullPolicy();
100+
ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy);
101+
Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder());
96102
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
103+
request = withRunImageIfNeeded(request, builderMetadata.getStack());
104+
Image runImage = imageFetcher.fetchImage(ImageType.RUNNER, request.getRunImage());
105+
assertStackIdsMatch(runImage, builderImage);
97106
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
98-
request = determineRunImage(request, builderImage, builderMetadata.getStack());
99-
EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getCreator(),
100-
request.getEnv());
101-
this.docker.image().load(builder.getArchive(), UpdateListener.none());
107+
Buildpacks buildpacks = getBuildpacks(request, imageFetcher, builderMetadata);
108+
EphemeralBuilder ephemeralBuilder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata,
109+
request.getCreator(), request.getEnv(), buildpacks);
110+
this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none());
102111
try {
103-
executeLifecycle(request, builder);
112+
executeLifecycle(request, ephemeralBuilder);
104113
if (request.isPublish()) {
105114
pushImage(request.getName());
106115
}
107116
}
108117
finally {
109-
this.docker.image().remove(builder.getName(), true);
118+
this.docker.image().remove(ephemeralBuilder.getName(), true);
110119
}
111120
}
112121

113-
private BuildRequest determineRunImage(BuildRequest request, Image builderImage, Stack builderStack)
114-
throws IOException {
115-
if (request.getRunImage() == null) {
116-
ImageReference runImage = getRunImageReferenceForStack(builderStack);
117-
request = request.withRunImage(runImage);
122+
private BuildRequest withRunImageIfNeeded(BuildRequest request, Stack builderStack) {
123+
if (request.getRunImage() != null) {
124+
return request;
118125
}
119-
assertImageRegistriesMatch(request);
120-
Image runImage = getImage(request, ImageType.RUNNER);
121-
assertStackIdsMatch(runImage, builderImage);
122-
return request;
126+
return request.withRunImage(getRunImageReferenceForStack(builderStack));
123127
}
124128

125129
private ImageReference getRunImageReferenceForStack(Stack stack) {
@@ -128,32 +132,22 @@ private ImageReference getRunImageReferenceForStack(Stack stack) {
128132
return ImageReference.of(name).inTaggedOrDigestForm();
129133
}
130134

131-
private Image getImage(BuildRequest request, ImageType imageType) throws IOException {
132-
ImageReference imageReference = (imageType == ImageType.BUILDER) ? request.getBuilder() : request.getRunImage();
133-
134-
if (request.getPullPolicy() == PullPolicy.ALWAYS) {
135-
return pullImage(imageReference, imageType);
136-
}
135+
private void assertStackIdsMatch(Image runImage, Image builderImage) {
136+
StackId runImageStackId = StackId.fromImage(runImage);
137+
StackId builderImageStackId = StackId.fromImage(builderImage);
138+
Assert.state(runImageStackId.equals(builderImageStackId), () -> "Run image stack '" + runImageStackId
139+
+ "' does not match builder stack '" + builderImageStackId + "'");
140+
}
137141

138-
try {
139-
return this.docker.image().inspect(imageReference);
140-
}
141-
catch (DockerEngineException exception) {
142-
if (request.getPullPolicy() == PullPolicy.IF_NOT_PRESENT && exception.getStatusCode() == 404) {
143-
return pullImage(imageReference, imageType);
144-
}
145-
else {
146-
throw exception;
147-
}
148-
}
142+
private Buildpacks getBuildpacks(BuildRequest request, ImageFetcher imageFetcher, BuilderMetadata builderMetadata) {
143+
BuildpackResolverContext resolverContext = new BuilderResolverContext(imageFetcher, builderMetadata);
144+
return BuildpackResolvers.resolveAll(resolverContext, request.getBuildpacks());
149145
}
150146

151-
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
152-
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingImage(reference, imageType);
153-
TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer);
154-
Image image = this.docker.image().pull(reference, listener, getBuilderAuthHeader());
155-
this.log.pulledImage(image, imageType);
156-
return image;
147+
private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException {
148+
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, builder)) {
149+
lifecycle.execute();
150+
}
157151
}
158152

159153
private void pushImage(ImageReference reference) throws IOException {
@@ -173,25 +167,83 @@ private String getPublishAuthHeader() {
173167
? this.dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader() : null;
174168
}
175169

176-
private void assertImageRegistriesMatch(BuildRequest request) {
177-
if (getBuilderAuthHeader() != null) {
178-
Assert.state(request.getRunImage().getDomain().equals(request.getBuilder().getDomain()),
179-
"Builder image '" + request.getBuilder() + "' and run image '" + request.getRunImage()
180-
+ "' must be pulled from the same authenticated registry");
170+
/**
171+
* Internal utility class used to fetch images.
172+
*/
173+
private class ImageFetcher {
174+
175+
private final String domain;
176+
177+
private final String authHeader;
178+
179+
private final PullPolicy pullPolicy;
180+
181+
ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy) {
182+
this.domain = domain;
183+
this.authHeader = authHeader;
184+
this.pullPolicy = pullPolicy;
185+
}
186+
187+
Image fetchImage(ImageType type, ImageReference reference) throws IOException {
188+
Assert.notNull(type, "Type must not be null");
189+
Assert.notNull(reference, "Reference must not be null");
190+
Assert.state(this.authHeader == null || reference.getDomain().equals(this.domain),
191+
() -> String.format("%s '%s' must be pulled from the '%s' authenticated registry",
192+
StringUtils.capitalize(type.getDescription()), reference, this.domain));
193+
if (this.pullPolicy == PullPolicy.ALWAYS) {
194+
return pullImage(reference, type);
195+
}
196+
try {
197+
return Builder.this.docker.image().inspect(reference);
198+
}
199+
catch (DockerEngineException ex) {
200+
if (this.pullPolicy == PullPolicy.IF_NOT_PRESENT && ex.getStatusCode() == 404) {
201+
return pullImage(reference, type);
202+
}
203+
throw ex;
204+
}
205+
}
206+
207+
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
208+
TotalProgressPullListener listener = new TotalProgressPullListener(
209+
Builder.this.log.pullingImage(reference, imageType));
210+
Image image = Builder.this.docker.image().pull(reference, listener, this.authHeader);
211+
Builder.this.log.pulledImage(image, imageType);
212+
return image;
181213
}
182-
}
183214

184-
private void assertStackIdsMatch(Image runImage, Image builderImage) {
185-
StackId runImageStackId = StackId.fromImage(runImage);
186-
StackId builderImageStackId = StackId.fromImage(builderImage);
187-
Assert.state(runImageStackId.equals(builderImageStackId), () -> "Run image stack '" + runImageStackId
188-
+ "' does not match builder stack '" + builderImageStackId + "'");
189215
}
190216

191-
private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException {
192-
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, builder)) {
193-
lifecycle.execute();
217+
/**
218+
* {@link BuildpackResolverContext} implementation for the {@link Builder}.
219+
*/
220+
private class BuilderResolverContext implements BuildpackResolverContext {
221+
222+
private final ImageFetcher imageFetcher;
223+
224+
private final BuilderMetadata builderMetadata;
225+
226+
BuilderResolverContext(ImageFetcher imageFetcher, BuilderMetadata builderMetadata) {
227+
this.imageFetcher = imageFetcher;
228+
this.builderMetadata = builderMetadata;
194229
}
230+
231+
@Override
232+
public List<BuildpackMetadata> getBuildpackMetadata() {
233+
return this.builderMetadata.getBuildpacks();
234+
}
235+
236+
@Override
237+
public Image fetchImage(ImageReference reference, ImageType imageType) throws IOException {
238+
return this.imageFetcher.fetchImage(imageType, reference);
239+
}
240+
241+
@Override
242+
public void exportImageLayers(ImageReference reference, IOBiConsumer<String, TarArchive> exports)
243+
throws IOException {
244+
Builder.this.docker.image().exportLayers(reference, exports);
245+
}
246+
195247
}
196248

197249
}

0 commit comments

Comments
 (0)