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
pull/34945/head
Scott Frederick 2 years ago
parent df542849be
commit 56bc6d2fa0

@ -83,6 +83,8 @@ public class BuildRequest {
private final Instant createdDate; private final Instant createdDate;
private final String applicationDirectory;
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) { BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
Assert.notNull(name, "Name must not be null"); Assert.notNull(name, "Name must not be null");
Assert.notNull(applicationContent, "ApplicationContent must not be null"); Assert.notNull(applicationContent, "ApplicationContent must not be null");
@ -103,13 +105,14 @@ public class BuildRequest {
this.buildCache = null; this.buildCache = null;
this.launchCache = null; this.launchCache = null;
this.createdDate = null; this.createdDate = null;
this.applicationDirectory = null;
} }
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder, BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache, ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks, boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks,
List<Binding> bindings, String network, List<ImageReference> tags, Cache buildCache, Cache launchCache, List<Binding> bindings, String network, List<ImageReference> tags, Cache buildCache, Cache launchCache,
Instant createdDate) { Instant createdDate, String applicationDirectory) {
this.name = name; this.name = name;
this.applicationContent = applicationContent; this.applicationContent = applicationContent;
this.builder = builder; this.builder = builder;
@ -127,6 +130,7 @@ public class BuildRequest {
this.buildCache = buildCache; this.buildCache = buildCache;
this.launchCache = launchCache; this.launchCache = launchCache;
this.createdDate = createdDate; this.createdDate = createdDate;
this.applicationDirectory = applicationDirectory;
} }
/** /**
@ -139,7 +143,7 @@ public class BuildRequest {
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache,
this.createdDate); this.createdDate, this.applicationDirectory);
} }
/** /**
@ -151,7 +155,7 @@ public class BuildRequest {
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache,
this.createdDate); this.createdDate, this.applicationDirectory);
} }
/** /**
@ -163,7 +167,8 @@ public class BuildRequest {
Assert.notNull(creator, "Creator must not be null"); Assert.notNull(creator, "Creator must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate); this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory);
} }
/** /**
@ -180,7 +185,7 @@ public class BuildRequest {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache, this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.launchCache,
this.createdDate); this.createdDate, this.applicationDirectory);
} }
/** /**
@ -195,7 +200,7 @@ public class BuildRequest {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy, Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy,
this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildCache, this.publish, this.buildpacks, this.bindings, this.network, this.tags, this.buildCache,
this.launchCache, this.createdDate); this.launchCache, this.createdDate, this.applicationDirectory);
} }
/** /**
@ -206,7 +211,8 @@ public class BuildRequest {
public BuildRequest withCleanCache(boolean cleanCache) { public BuildRequest withCleanCache(boolean cleanCache) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate); this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory);
} }
/** /**
@ -217,7 +223,8 @@ public class BuildRequest {
public BuildRequest withVerboseLogging(boolean verboseLogging) { public BuildRequest withVerboseLogging(boolean verboseLogging) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate); this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory);
} }
/** /**
@ -228,7 +235,8 @@ public class BuildRequest {
public BuildRequest withPullPolicy(PullPolicy pullPolicy) { public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings, this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate); this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory);
} }
/** /**
@ -239,7 +247,8 @@ public class BuildRequest {
public BuildRequest withPublish(boolean publish) { public BuildRequest withPublish(boolean publish) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings, this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate); this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory);
} }
/** /**
@ -263,7 +272,8 @@ public class BuildRequest {
Assert.notNull(buildpacks, "Buildpacks must not be null"); Assert.notNull(buildpacks, "Buildpacks must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate); this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory);
} }
/** /**
@ -287,7 +297,8 @@ public class BuildRequest {
Assert.notNull(bindings, "Bindings must not be null"); Assert.notNull(bindings, "Bindings must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate); this.network, this.tags, this.buildCache, this.launchCache, this.createdDate,
this.applicationDirectory);
} }
/** /**
@ -299,7 +310,7 @@ public class BuildRequest {
public BuildRequest withNetwork(String network) { public BuildRequest withNetwork(String network) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
network, this.tags, this.buildCache, this.launchCache, this.createdDate); network, this.tags, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory);
} }
/** /**
@ -321,7 +332,7 @@ public class BuildRequest {
Assert.notNull(tags, "Tags must not be null"); Assert.notNull(tags, "Tags must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, tags, this.buildCache, this.launchCache, this.createdDate); this.network, tags, this.buildCache, this.launchCache, this.createdDate, this.applicationDirectory);
} }
/** /**
@ -333,7 +344,7 @@ public class BuildRequest {
Assert.notNull(buildCache, "BuildCache must not be null"); Assert.notNull(buildCache, "BuildCache must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, buildCache, this.launchCache, this.createdDate); this.network, this.tags, buildCache, this.launchCache, this.createdDate, this.applicationDirectory);
} }
/** /**
@ -345,7 +356,7 @@ public class BuildRequest {
Assert.notNull(launchCache, "LaunchCache must not be null"); Assert.notNull(launchCache, "LaunchCache must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, launchCache, this.createdDate); this.network, this.tags, this.buildCache, launchCache, this.createdDate, this.applicationDirectory);
} }
/** /**
@ -357,7 +368,8 @@ public class BuildRequest {
Assert.notNull(createdDate, "CreatedDate must not be null"); Assert.notNull(createdDate, "CreatedDate must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, parseCreatedDate(createdDate)); this.network, this.tags, this.buildCache, this.launchCache, parseCreatedDate(createdDate),
this.applicationDirectory);
} }
private Instant parseCreatedDate(String createdDate) { private Instant parseCreatedDate(String createdDate) {
@ -372,6 +384,18 @@ public class BuildRequest {
} }
} }
/**
* Return a new {@link BuildRequest} with an updated application directory.
* @param applicationDirectory the application directory
* @return an updated build request
*/
public BuildRequest withApplicationDirectory(String applicationDirectory) {
Assert.notNull(applicationDirectory, "ApplicationDirectory must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.network, this.tags, this.buildCache, this.launchCache, this.createdDate, applicationDirectory);
}
/** /**
* Return the name of the image that should be created. * Return the name of the image that should be created.
* @return the name of the image * @return the name of the image
@ -513,6 +537,14 @@ public class BuildRequest {
return this.createdDate; return this.createdDate;
} }
/**
* Return the application directory that should be used by the lifecycle.
* @return the application directory
*/
public String getApplicationDirectory() {
return this.applicationDirectory;
}
/** /**
* Factory method to create a new {@link BuildRequest} from a JAR file. * Factory method to create a new {@link BuildRequest} from a JAR file.
* @param jarFile the source jar file * @param jarFile the source jar file

@ -76,6 +76,8 @@ class Lifecycle implements Closeable {
private final VolumeName launchCacheVolume; private final VolumeName launchCacheVolume;
private final String applicationDirectory;
private boolean executed; private boolean executed;
private boolean applicationVolumePopulated; private boolean applicationVolumePopulated;
@ -101,6 +103,7 @@ class Lifecycle implements Closeable {
this.applicationVolume = createRandomVolumeName("pack-app-"); this.applicationVolume = createRandomVolumeName("pack-app-");
this.buildCacheVolume = getBuildCacheVolumeName(request); this.buildCacheVolume = getBuildCacheVolumeName(request);
this.launchCacheVolume = getLaunchCacheVolumeName(request); this.launchCacheVolume = getLaunchCacheVolumeName(request);
this.applicationDirectory = getApplicationDirectory(request);
} }
protected VolumeName createRandomVolumeName(String prefix) { protected VolumeName createRandomVolumeName(String prefix) {
@ -128,6 +131,10 @@ class Lifecycle implements Closeable {
return null; return null;
} }
private String getApplicationDirectory(BuildRequest request) {
return (request.getApplicationDirectory() != null) ? request.getApplicationDirectory() : Directory.APPLICATION;
}
private VolumeName createCacheVolumeName(BuildRequest request, String suffix) { private VolumeName createCacheVolumeName(BuildRequest request, String suffix) {
return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6); return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6);
} }
@ -161,7 +168,7 @@ class Lifecycle implements Closeable {
phase.withDaemonAccess(); phase.withDaemonAccess();
configureDaemonAccess(phase); configureDaemonAccess(phase);
phase.withLogLevelArg(); phase.withLogLevelArg();
phase.withArgs("-app", Directory.APPLICATION); phase.withArgs("-app", this.applicationDirectory);
phase.withArgs("-platform", Directory.PLATFORM); phase.withArgs("-platform", Directory.PLATFORM);
phase.withArgs("-run-image", this.request.getRunImage()); phase.withArgs("-run-image", this.request.getRunImage());
phase.withArgs("-layers", Directory.LAYERS); phase.withArgs("-layers", Directory.LAYERS);
@ -176,7 +183,7 @@ class Lifecycle implements Closeable {
} }
phase.withArgs(this.request.getName()); phase.withArgs(this.request.getName());
phase.withBinding(Binding.from(this.layersVolume, Directory.LAYERS)); phase.withBinding(Binding.from(this.layersVolume, Directory.LAYERS));
phase.withBinding(Binding.from(this.applicationVolume, Directory.APPLICATION)); phase.withBinding(Binding.from(this.applicationVolume, this.applicationDirectory));
phase.withBinding(Binding.from(this.buildCacheVolume, Directory.CACHE)); phase.withBinding(Binding.from(this.buildCacheVolume, Directory.CACHE));
phase.withBinding(Binding.from(this.launchCacheVolume, Directory.LAUNCH_CACHE)); phase.withBinding(Binding.from(this.launchCacheVolume, Directory.LAUNCH_CACHE));
if (this.request.getBindings() != null) { if (this.request.getBindings() != null) {
@ -245,7 +252,7 @@ class Lifecycle implements Closeable {
try { try {
TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner()); TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner());
return this.docker.container() return this.docker.container()
.create(config, ContainerContent.of(applicationContent, Directory.APPLICATION)); .create(config, ContainerContent.of(applicationContent, this.applicationDirectory));
} }
finally { finally {
this.applicationVolumePopulated = true; this.applicationVolumePopulated = true;

@ -292,6 +292,13 @@ class BuildRequestTests {
.withMessageContaining("'not a date'"); .withMessageContaining("'not a date'");
} }
@Test
void withApplicationDirectorySetsApplicationDirectory() throws Exception {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
BuildRequest withAppDir = request.withApplicationDirectory("/application");
assertThat(withAppDir.getApplicationDirectory()).isEqualTo("/application");
}
private void hasExpectedJarContent(TarArchive archive) { private void hasExpectedJarContent(TarArchive archive) {
try { try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

@ -229,6 +229,17 @@ class LifecycleTests {
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
} }
@Test
void executeWithApplicationDirectoryExecutesPhases() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null));
BuildRequest request = getTestRequest().withApplicationDirectory("/application");
createLifecycle(request).execute();
assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-app-dir.json"));
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
}
@Test @Test
void executeWithDockerHostAndRemoteAddressExecutesPhases() throws Exception { void executeWithDockerHostAndRemoteAddressExecutesPhases() throws Exception {
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());

@ -0,0 +1,39 @@
{
"User": "root",
"Image": "pack.local/ephemeral-builder",
"Cmd": [
"/cnb/lifecycle/creator",
"-app",
"/application",
"-platform",
"/platform",
"-run-image",
"docker.io/cloudfoundry/run:latest",
"-layers",
"/layers",
"-cache-dir",
"/cache",
"-launch-cache",
"/launch-cache",
"-daemon",
"docker.io/library/my-application:latest"
],
"Env": [
"CNB_PLATFORM_API=0.8"
],
"Labels": {
"author": "spring-boot"
},
"HostConfig": {
"Binds": [
"/var/run/docker.sock:/var/run/docker.sock",
"pack-layers-aaaaaaaaaa:/layers",
"pack-app-aaaaaaaaaa:/application",
"pack-cache-b35197ac41ea.build:/cache",
"pack-cache-b35197ac41ea.launch:/launch-cache"
],
"SecurityOpt" : [
"label=disable"
]
}
}

@ -199,6 +199,12 @@ The values provided to the `tags` option should be full image references in the
The value must be a string in the ISO 8601 instant format, or `now` to use the current date and time. The value must be a string in the ISO 8601 instant format, or `now` to use the current date and time.
| A fixed date that enables https://buildpacks.io/docs/features/reproducibility/[build reproducibility]. | A fixed date that enables https://buildpacks.io/docs/features/reproducibility/[build reproducibility].
| `applicationDirectory`
| `--applicationDirectory`
| The path to a directory that application contents will be uploaded to in the builder image.
Application contents will also be in this location in the generated image.
| `/workspace`
|=== |===
NOTE: The plugin detects the target Java compatibility of the project using the JavaPlugin's `targetCompatibility` property. NOTE: The plugin detects the target Java compatibility of the project using the JavaPlugin's `targetCompatibility` property.

@ -270,6 +270,16 @@ public abstract class BootBuildImage extends DefaultTask {
@Option(option = "createdDate", description = "The date to use as the created date of the image") @Option(option = "createdDate", description = "The date to use as the created date of the image")
public abstract Property<String> getCreatedDate(); public abstract Property<String> getCreatedDate();
/**
* Returns the directory that contains application content in the image. When
* {@code null}, a default location will be used.
* @return the application directory
*/
@Input
@Optional
@Option(option = "applicationDirectory", description = "The directory containing application content in the image")
public abstract Property<String> getApplicationDirectory();
/** /**
* Returns the Docker configuration the builder will use. * Returns the Docker configuration the builder will use.
* @return docker configuration. * @return docker configuration.
@ -316,6 +326,7 @@ public abstract class BootBuildImage extends DefaultTask {
request = customizeCaches(request); request = customizeCaches(request);
request = request.withNetwork(getNetwork().getOrNull()); request = request.withNetwork(getNetwork().getOrNull());
request = customizeCreatedDate(request); request = customizeCreatedDate(request);
request = customizeApplicationDirectory(request);
return request; return request;
} }
@ -406,4 +417,12 @@ public abstract class BootBuildImage extends DefaultTask {
return request; return request;
} }
private BuildRequest customizeApplicationDirectory(BuildRequest request) {
String applicationDirectory = getApplicationDirectory().getOrNull();
if (applicationDirectory != null) {
return request.withApplicationDirectory(applicationDirectory);
}
return request;
}
} }

@ -143,8 +143,8 @@ class BootBuildImageIntegrationTests {
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT", BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT",
"--imageName=example/test-image-cmd", "--imageName=example/test-image-cmd",
"--builder=projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2", "--builder=projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2",
"--runImage=projects.registry.vmware.com/springboot/run:tiny-cnb", "--runImage=projects.registry.vmware.com/springboot/run:tiny-cnb", "--createdDate=2020-07-01T12:34:56Z",
"--createdDate=2020-07-01T12:34:56Z"); "--applicationDirectory=/application");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("example/test-image-cmd"); assertThat(result.getOutput()).contains("example/test-image-cmd");
assertThat(result.getOutput()).contains("---> Test Info buildpack building"); assertThat(result.getOutput()).contains("---> Test Info buildpack building");
@ -329,6 +329,19 @@ class BootBuildImageIntegrationTests {
removeImages(projectName); removeImages(projectName);
} }
@TestTemplate
void buildsImageWithApplicationDirectory() throws IOException {
writeMainClass();
writeLongNameResource();
BuildResult result = this.gradleBuild.build("bootBuildImage");
String projectName = this.gradleBuild.getProjectDir().getName();
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
removeImages(projectName);
}
@TestTemplate @TestTemplate
void failsWithInvalidCreatedDate() throws IOException { void failsWithInvalidCreatedDate() throws IOException {
writeMainClass(); writeMainClass();

@ -0,0 +1,14 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
if (project.hasProperty('applyWarPlugin')) {
apply plugin: 'war'
}
bootBuildImage {
builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2"
pullPolicy = "IF_NOT_PRESENT"
applicationDirectory = "/application"
}

@ -209,6 +209,13 @@ The values provided to the `tags` option should be full image references in the
The value must be a string in the ISO 8601 instant format, or `now` to use the current date and time. The value must be a string in the ISO 8601 instant format, or `now` to use the current date and time.
| A fixed date that enables https://buildpacks.io/docs/features/reproducibility/[build reproducibility]. | A fixed date that enables https://buildpacks.io/docs/features/reproducibility/[build reproducibility].
| `applicationDirectory` +
(`spring-boot.build-image.applicationDirectory`)
| The path to a directory that application contents will be uploaded to in the builder image.
Application contents will also be in this location in the generated image.
| `/workspace`
|=== |===
NOTE: The plugin detects the target Java compatibility of the project using the compiler's plugin configuration or the `maven.compiler.target` property. NOTE: The plugin detects the target Java compatibility of the project using the compiler's plugin configuration or the `maven.compiler.target` property.

@ -245,6 +245,7 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
"projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2") "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2")
.systemProperty("spring-boot.build-image.runImage", "projects.registry.vmware.com/springboot/run:tiny-cnb") .systemProperty("spring-boot.build-image.runImage", "projects.registry.vmware.com/springboot/run:tiny-cnb")
.systemProperty("spring-boot.build-image.createdDate", "2020-07-01T12:34:56Z") .systemProperty("spring-boot.build-image.createdDate", "2020-07-01T12:34:56Z")
.systemProperty("spring-boot.build-image.applicationDirectory", "/application")
.execute((project) -> { .execute((project) -> {
assertThat(buildLog(project)).contains("Building image") assertThat(buildLog(project)).contains("Building image")
.contains("example.com/test/cmd-property-name:v1") .contains("example.com/test/cmd-property-name:v1")
@ -434,6 +435,21 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
}); });
} }
@TestTemplate
void whenBuildImageIsInvokedWithApplicationDirectory(MavenBuild mavenBuild) {
String testBuildId = randomString();
mavenBuild.project("build-image-app-dir")
.goals("package")
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT")
.systemProperty("test-build-id", testBuildId)
.execute((project) -> {
assertThat(buildLog(project)).contains("Building image")
.contains("docker.io/library/build-image-app-dir:0.0.1.BUILD-SNAPSHOT")
.contains("Successfully built image");
removeImage("build-image-app-dir", "0.0.1.BUILD-SNAPSHOT");
});
}
@TestTemplate @TestTemplate
void failsWhenBuildImageIsInvokedOnMultiModuleProjectWithBuildImageGoal(MavenBuild mavenBuild) { void failsWhenBuildImageIsInvokedOnMultiModuleProjectWithBuildImageGoal(MavenBuild mavenBuild) {
mavenBuild.project("build-image-multi-module") mavenBuild.project("build-image-multi-module")

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot.maven.it</groupId>
<artifactId>build-image-app-dir</artifactId>
<version>0.0.1.BUILD-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>@java.version@</maven.compiler.source>
<maven.compiler.target>@java.version@</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>build-image-no-fork</goal>
</goals>
<configuration>
<image>
<builder>projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.2</builder>
<applicationDirectory>/application</applicationDirectory>
</image>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,28 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.test;
public class SampleApplication {
public static void main(String[] args) throws Exception {
System.out.println("Launched");
synchronized(args) {
args.wait(); // Prevent exit"
}
}
}

@ -163,6 +163,14 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo {
@Parameter(property = "spring-boot.build-image.createdDate", readonly = true) @Parameter(property = "spring-boot.build-image.createdDate", readonly = true)
String createdDate; String createdDate;
/**
* Alias for {@link Image#applicationDirectory} to support configuration through
* command-line property.
* @since 3.1.0
*/
@Parameter(property = "spring-boot.build-image.applicationDirectory", readonly = true)
String applicationDirectory;
/** /**
* Docker configuration options. * Docker configuration options.
* @since 2.4.0 * @since 2.4.0
@ -264,6 +272,9 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo {
if (image.createdDate == null && this.createdDate != null) { if (image.createdDate == null && this.createdDate != null) {
image.setCreatedDate(this.createdDate); image.setCreatedDate(this.createdDate);
} }
if (image.applicationDirectory == null && this.applicationDirectory != null) {
image.setApplicationDirectory(this.applicationDirectory);
}
return customize(image.getBuildRequest(this.project.getArtifact(), content)); return customize(image.getBuildRequest(this.project.getArtifact(), content));
} }

@ -75,6 +75,8 @@ public class Image {
String createdDate; String createdDate;
String applicationDirectory;
/** /**
* The name of the created image. * The name of the created image.
* @return the image name * @return the image name
@ -187,6 +189,18 @@ public class Image {
this.createdDate = createdDate; this.createdDate = createdDate;
} }
/**
* Returns the application content directory for the image.
* @return the application directory
*/
public String getApplicationDirectory() {
return this.applicationDirectory;
}
public void setApplicationDirectory(String applicationDirectory) {
this.applicationDirectory = applicationDirectory;
}
BuildRequest getBuildRequest(Artifact artifact, Function<Owner, TarArchive> applicationContent) { BuildRequest getBuildRequest(Artifact artifact, Function<Owner, TarArchive> applicationContent) {
return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent)); return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent));
} }
@ -238,6 +252,9 @@ public class Image {
if (StringUtils.hasText(this.createdDate)) { if (StringUtils.hasText(this.createdDate)) {
request = request.withCreatedDate(this.createdDate); request = request.withCreatedDate(this.createdDate);
} }
if (StringUtils.hasText(this.applicationDirectory)) {
request = request.withApplicationDirectory(this.applicationDirectory);
}
return request; return request;
} }

@ -193,6 +193,14 @@ class ImageTests {
assertThat(request.getCreatedDate()).isEqualTo("2020-07-01T12:34:56Z"); assertThat(request.getCreatedDate()).isEqualTo("2020-07-01T12:34:56Z");
} }
@Test
void getBuildRequestWhenHasApplicationDirectoryUsesApplicationDirectory() {
Image image = new Image();
image.applicationDirectory = "/application";
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getApplicationDirectory()).isEqualTo("/application");
}
private Artifact createArtifact() { private Artifact createArtifact() {
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile", return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
"jar", null, new DefaultArtifactHandler()); "jar", null, new DefaultArtifactHandler());

Loading…
Cancel
Save