Skip to content

Commit 56bc6d2

Browse files
Allow image application directory to be configurable
An `applicationDirectory` option on the Maven `spring-boot:build-image` goal and the Gradle `bootBuildImage` task can be configured to set the location that will be used to upload application contents to the builder image, and will contain the application contents in the generated image. Closes gh-34786
1 parent df54284 commit 56bc6d2

File tree

16 files changed

+292
-22
lines changed

16 files changed

+292
-22
lines changed

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 & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ public class BuildRequest {
8383

8484
private final Instant createdDate;
8585

86+
private final String applicationDirectory;
87+
8688
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
8789
Assert.notNull(name, "Name must not be null");
8890
Assert.notNull(applicationContent, "ApplicationContent must not be null");
@@ -103,13 +105,14 @@ public class BuildRequest {
103105
this.buildCache = null;
104106
this.launchCache = null;
105107
this.createdDate = null;
108+
this.applicationDirectory = null;
106109
}
107110

108111
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
109112
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
110113
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks,
111114
List<Binding> bindings, String network, List<ImageReference> tags, Cache buildCache, Cache launchCache,
112-
Instant createdDate) {
115+
Instant createdDate, String applicationDirectory) {
113116
this.name = name;
114117
this.applicationContent = applicationContent;
115118
this.builder = builder;
@@ -127,6 +130,7 @@ public class BuildRequest {
127130
this.buildCache = buildCache;
128131
this.launchCache = launchCache;
129132
this.createdDate = createdDate;
133+
this.applicationDirectory = applicationDirectory;
130134
}
131135

132136
/**
@@ -139,7 +143,7 @@ public BuildRequest withBuilder(ImageReference builder) {
139143
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
140144
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
141145
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache,
142-
this.createdDate);
146+
this.createdDate, this.applicationDirectory);
143147
}
144148

145149
/**
@@ -151,7 +155,7 @@ public BuildRequest withRunImage(ImageReference runImageName) {
151155
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
152156
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
153157
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache,
154-
this.createdDate);
158+
this.createdDate, this.applicationDirectory);
155159
}
156160

157161
/**
@@ -163,7 +167,8 @@ public BuildRequest withCreator(Creator creator) {
163167
Assert.notNull(creator, "Creator must not be null");
164168
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
165169
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
166-
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate);
170+
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
171+
this.applicationDirectory);
167172
}
168173

169174
/**
@@ -180,7 +185,7 @@ public BuildRequest withEnv(String name, String value) {
180185
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
181186
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
182187
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache,
183-
this.createdDate);
188+
this.createdDate, this.applicationDirectory);
184189
}
185190

186191
/**
@@ -195,7 +200,7 @@ public BuildRequest withEnv(Map<String, String> env) {
195200
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
196201
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy,
197202
this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildCache,
198-
this.launchCache, this.createdDate);
203+
this.launchCache, this.createdDate, this.applicationDirectory);
199204
}
200205

201206
/**
@@ -206,7 +211,8 @@ public BuildRequest withEnv(Map<String, String> env) {
206211
public BuildRequest withCleanCache(boolean cleanCache) {
207212
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
208213
cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
209-
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate);
214+
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
215+
this.applicationDirectory);
210216
}
211217

212218
/**
@@ -217,7 +223,8 @@ public BuildRequest withCleanCache(boolean cleanCache) {
217223
public BuildRequest withVerboseLogging(boolean verboseLogging) {
218224
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
219225
this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
220-
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate);
226+
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
227+
this.applicationDirectory);
221228
}
222229

223230
/**
@@ -228,7 +235,8 @@ public BuildRequest withVerboseLogging(boolean verboseLogging) {
228235
public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
229236
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
230237
this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings,
231-
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate);
238+
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
239+
this.applicationDirectory);
232240
}
233241

234242
/**
@@ -239,7 +247,8 @@ public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
239247
public BuildRequest withPublish(boolean publish) {
240248
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
241249
this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings,
242-
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate);
250+
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
251+
this.applicationDirectory);
243252
}
244253

245254
/**
@@ -263,7 +272,8 @@ public BuildRequest withBuildpacks(List<BuildpackReference> buildpacks) {
263272
Assert.notNull(buildpacks, "Buildpacks must not be null");
264273
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
265274
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings,
266-
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate);
275+
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
276+
this.applicationDirectory);
267277
}
268278

269279
/**
@@ -287,7 +297,8 @@ public BuildRequest withBindings(List<Binding> bindings) {
287297
Assert.notNull(bindings, "Bindings must not be null");
288298
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
289299
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings,
290-
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate);
300+
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
301+
this.applicationDirectory);
291302
}
292303

293304
/**
@@ -299,7 +310,7 @@ public BuildRequest withBindings(List<Binding> bindings) {
299310
public BuildRequest withNetwork(String network) {
300311
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
301312
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
302-
network, this.tags, this.buildCache, this.launchCache, this.createdDate);
313+
network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory);
303314
}
304315

305316
/**
@@ -321,7 +332,7 @@ public BuildRequest withTags(List<ImageReference> tags) {
321332
Assert.notNull(tags, "Tags must not be null");
322333
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
323334
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
324-
this.network, tags, this.buildCache, this.launchCache, this.createdDate);
335+
this.network, tags, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory);
325336
}
326337

327338
/**
@@ -333,7 +344,7 @@ public BuildRequest withBuildCache(Cache buildCache) {
333344
Assert.notNull(buildCache, "BuildCache must not be null");
334345
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
335346
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
336-
this.network, this.tags, buildCache, this.launchCache, this.createdDate);
347+
this.network, this.tags, buildCache, this.launchCache, this.createdDate, this.applicationDirectory);
337348
}
338349

339350
/**
@@ -345,7 +356,7 @@ public BuildRequest withLaunchCache(Cache launchCache) {
345356
Assert.notNull(launchCache, "LaunchCache must not be null");
346357
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
347358
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
348-
this.network, this.tags, this.buildCache, launchCache, this.createdDate);
359+
this.network, this.tags, this.buildCache, launchCache, this.createdDate, this.applicationDirectory);
349360
}
350361

351362
/**
@@ -357,7 +368,8 @@ public BuildRequest withCreatedDate(String createdDate) {
357368
Assert.notNull(createdDate, "CreatedDate must not be null");
358369
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
359370
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
360-
this.network, this.tags, this.buildCache, this.launchCache, parseCreatedDate(createdDate));
371+
this.network, this.tags, this.buildCache, this.launchCache, parseCreatedDate(createdDate),
372+
this.applicationDirectory);
361373
}
362374

363375
private Instant parseCreatedDate(String createdDate) {
@@ -372,6 +384,18 @@ private Instant parseCreatedDate(String createdDate) {
372384
}
373385
}
374386

387+
/**
388+
* Return a new {@link BuildRequest} with an updated application directory.
389+
* @param applicationDirectory the application directory
390+
* @return an updated build request
391+
*/
392+
public BuildRequest withApplicationDirectory(String applicationDirectory) {
393+
Assert.notNull(applicationDirectory, "ApplicationDirectory must not be null");
394+
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
395+
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
396+
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, applicationDirectory);
397+
}
398+
375399
/**
376400
* Return the name of the image that should be created.
377401
* @return the name of the image
@@ -513,6 +537,14 @@ public Instant getCreatedDate() {
513537
return this.createdDate;
514538
}
515539

540+
/**
541+
* Return the application directory that should be used by the lifecycle.
542+
* @return the application directory
543+
*/
544+
public String getApplicationDirectory() {
545+
return this.applicationDirectory;
546+
}
547+
516548
/**
517549
* Factory method to create a new {@link BuildRequest} from a JAR file.
518550
* @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/Lifecycle.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ class Lifecycle implements Closeable {
7676

7777
private final VolumeName launchCacheVolume;
7878

79+
private final String applicationDirectory;
80+
7981
private boolean executed;
8082

8183
private boolean applicationVolumePopulated;
@@ -101,6 +103,7 @@ class Lifecycle implements Closeable {
101103
this.applicationVolume = createRandomVolumeName("pack-app-");
102104
this.buildCacheVolume = getBuildCacheVolumeName(request);
103105
this.launchCacheVolume = getLaunchCacheVolumeName(request);
106+
this.applicationDirectory = getApplicationDirectory(request);
104107
}
105108

106109
protected VolumeName createRandomVolumeName(String prefix) {
@@ -128,6 +131,10 @@ private VolumeName getVolumeName(Cache cache) {
128131
return null;
129132
}
130133

134+
private String getApplicationDirectory(BuildRequest request) {
135+
return (request.getApplicationDirectory() != null) ? request.getApplicationDirectory() : Directory.APPLICATION;
136+
}
137+
131138
private VolumeName createCacheVolumeName(BuildRequest request, String suffix) {
132139
return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6);
133140
}
@@ -161,7 +168,7 @@ private Phase createPhase() {
161168
phase.withDaemonAccess();
162169
configureDaemonAccess(phase);
163170
phase.withLogLevelArg();
164-
phase.withArgs("-app", Directory.APPLICATION);
171+
phase.withArgs("-app", this.applicationDirectory);
165172
phase.withArgs("-platform", Directory.PLATFORM);
166173
phase.withArgs("-run-image", this.request.getRunImage());
167174
phase.withArgs("-layers", Directory.LAYERS);
@@ -176,7 +183,7 @@ private Phase createPhase() {
176183
}
177184
phase.withArgs(this.request.getName());
178185
phase.withBinding(Binding.from(this.layersVolume, Directory.LAYERS));
179-
phase.withBinding(Binding.from(this.applicationVolume, Directory.APPLICATION));
186+
phase.withBinding(Binding.from(this.applicationVolume, this.applicationDirectory));
180187
phase.withBinding(Binding.from(this.buildCacheVolume, Directory.CACHE));
181188
phase.withBinding(Binding.from(this.launchCacheVolume, Directory.LAUNCH_CACHE));
182189
if (this.request.getBindings() != null) {
@@ -245,7 +252,7 @@ private ContainerReference createContainer(ContainerConfig config) throws IOExce
245252
try {
246253
TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner());
247254
return this.docker.container()
248-
.create(config, ContainerContent.of(applicationContent, Directory.APPLICATION));
255+
.create(config, ContainerContent.of(applicationContent, this.applicationDirectory));
249256
}
250257
finally {
251258
this.applicationVolumePopulated = true;

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,13 @@ void withCreatedDateAndInvalidDateThrowsException() throws Exception {
292292
.withMessageContaining("'not a date'");
293293
}
294294

295+
@Test
296+
void withApplicationDirectorySetsApplicationDirectory() throws Exception {
297+
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
298+
BuildRequest withAppDir = request.withApplicationDirectory("/application");
299+
assertThat(withAppDir.getApplicationDirectory()).isEqualTo("/application");
300+
}
301+
295302
private void hasExpectedJarContent(TarArchive archive) {
296303
try {
297304
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,17 @@ void executeWithCreatedDateExecutesPhases() throws Exception {
229229
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
230230
}
231231

232+
@Test
233+
void executeWithApplicationDirectoryExecutesPhases() throws Exception {
234+
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
235+
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
236+
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
237+
BuildRequest request = getTestRequest().withApplicationDirectory("/application");
238+
createLifecycle(request).execute();
239+
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-app-dir.json"));
240+
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
241+
}
242+
232243
@Test
233244
void executeWithDockerHostAndRemoteAddressExecutesPhases() throws Exception {
234245
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"User": "root",
3+
"Image": "pack.local/ephemeral-builder",
4+
"Cmd": [
5+
"/cnb/lifecycle/creator",
6+
"-app",
7+
"/application",
8+
"-platform",
9+
"/platform",
10+
"-run-image",
11+
"docker.io/cloudfoundry/run:latest",
12+
"-layers",
13+
"/layers",
14+
"-cache-dir",
15+
"/cache",
16+
"-launch-cache",
17+
"/launch-cache",
18+
"-daemon",
19+
"docker.io/library/my-application:latest"
20+
],
21+
"Env": [
22+
"CNB_PLATFORM_API=0.8"
23+
],
24+
"Labels": {
25+
"author": "spring-boot"
26+
},
27+
"HostConfig": {
28+
"Binds": [
29+
"/var/run/docker.sock:/var/run/docker.sock",
30+
"pack-layers-aaaaaaaaaa:/layers",
31+
"pack-app-aaaaaaaaaa:/application",
32+
"pack-cache-b35197ac41ea.build:/cache",
33+
"pack-cache-b35197ac41ea.launch:/launch-cache"
34+
],
35+
"SecurityOpt" : [
36+
"label=disable"
37+
]
38+
}
39+
}

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,12 @@ The values provided to the `tags` option should be full image references in the
199199
The value must be a string in the ISO 8601 instant format, or `now` to use the current date and time.
200200
| A fixed date that enables https://buildpacks.io/docs/features/reproducibility/[build reproducibility].
201201

202+
| `applicationDirectory`
203+
| `--applicationDirectory`
204+
| The path to a directory that application contents will be uploaded to in the builder image.
205+
Application contents will also be in this location in the generated image.
206+
| `/workspace`
207+
202208
|===
203209

204210
NOTE: The plugin detects the target Java compatibility of the project using the JavaPlugin's `targetCompatibility` property.

spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,16 @@ public void launchCache(Action<CacheSpec> action) {
270270
@Option(option = "createdDate", description = "The date to use as the created date of the image")
271271
public abstract Property<String> getCreatedDate();
272272

273+
/**
274+
* Returns the directory that contains application content in the image. When
275+
* {@code null}, a default location will be used.
276+
* @return the application directory
277+
*/
278+
@Input
279+
@Optional
280+
@Option(option = "applicationDirectory", description = "The directory containing application content in the image")
281+
public abstract Property<String> getApplicationDirectory();
282+
273283
/**
274284
* Returns the Docker configuration the builder will use.
275285
* @return docker configuration.
@@ -316,6 +326,7 @@ private BuildRequest customize(BuildRequest request) {
316326
request = customizeCaches(request);
317327
request = request.withNetwork(getNetwork().getOrNull());
318328
request = customizeCreatedDate(request);
329+
request = customizeApplicationDirectory(request);
319330
return request;
320331
}
321332

@@ -406,4 +417,12 @@ private BuildRequest customizeCreatedDate(BuildRequest request) {
406417
return request;
407418
}
408419

420+
private BuildRequest customizeApplicationDirectory(BuildRequest request) {
421+
String applicationDirectory = getApplicationDirectory().getOrNull();
422+
if (applicationDirectory != null) {
423+
return request.withApplicationDirectory(applicationDirectory);
424+
}
425+
return request;
426+
}
427+
409428
}

0 commit comments

Comments
 (0)