Add runImage option for image building

This commit adds a runImage property to the Maven plugin build-image
goal and the Gradle bootBuildImage task. The property allows the user
to override the run image reference provided in the builder metadata
with an alternate run image. The runImage property can be specified
in the build file or on the command line.

Fixes gh-21534
pull/22035/head
Scott Frederick 4 years ago
parent 025d7aaac8
commit 6119d69679

@ -46,6 +46,8 @@ public class BuildRequest {
private final ImageReference builder;
private final ImageReference runImage;
private final Creator creator;
private final Map<String, String> env;
@ -60,6 +62,7 @@ public class BuildRequest {
this.name = name.inTaggedForm();
this.applicationContent = applicationContent;
this.builder = DEFAULT_BUILDER;
this.runImage = null;
this.env = Collections.emptyMap();
this.cleanCache = false;
this.verboseLogging = false;
@ -67,10 +70,12 @@ public class BuildRequest {
}
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
Creator creator, Map<String, String> env, boolean cleanCache, boolean verboseLogging) {
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
boolean verboseLogging) {
this.name = name;
this.applicationContent = applicationContent;
this.builder = builder;
this.runImage = runImage;
this.creator = creator;
this.env = env;
this.cleanCache = cleanCache;
@ -84,20 +89,29 @@ public class BuildRequest {
*/
public BuildRequest withBuilder(ImageReference builder) {
Assert.notNull(builder, "Builder must not be null");
builder = (builder.getDigest() != null) ? builder : builder.inTaggedForm();
return new BuildRequest(this.name, this.applicationContent, builder, this.creator, this.env, this.cleanCache,
this.verboseLogging);
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging);
}
/**
* Return a new {@link BuildRequest} with an updated builder.
* Return a new {@link BuildRequest} with an updated run image.
* @param runImageName the run image to use
* @return an updated build request
*/
public BuildRequest withRunImage(ImageReference runImageName) {
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
this.creator, this.env, this.cleanCache, this.verboseLogging);
}
/**
* Return a new {@link BuildRequest} with an updated creator.
* @param creator the new {@code Creator} to use
* @return an updated build request
*/
public BuildRequest withCreator(Creator creator) {
Assert.notNull(creator, "Creator must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, creator, this.env, this.cleanCache,
this.verboseLogging);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
this.cleanCache, this.verboseLogging);
}
/**
@ -111,12 +125,12 @@ public class BuildRequest {
Assert.hasText(value, "Value must not be empty");
Map<String, String> env = new LinkedHashMap<>(this.env);
env.put(name, value);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.creator,
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging);
}
/**
* Return a new {@link BuildRequest} with an additional env variables.
* Return a new {@link BuildRequest} with additional env variables.
* @param env the additional variables
* @return an updated build request
*/
@ -124,27 +138,27 @@ public class BuildRequest {
Assert.notNull(env, "Env must not be null");
Map<String, String> updatedEnv = new LinkedHashMap<>(this.env);
updatedEnv.putAll(env);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.creator,
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging);
}
/**
* Return a new {@link BuildRequest} with an specific clean cache settings.
* Return a new {@link BuildRequest} with an updated clean cache setting.
* @param cleanCache if the cache should be cleaned
* @return an updated build request
*/
public BuildRequest withCleanCache(boolean cleanCache) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.creator, this.env, cleanCache,
this.verboseLogging);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
cleanCache, this.verboseLogging);
}
/**
* Return a new {@link BuildRequest} with an specific verbose logging settings.
* Return a new {@link BuildRequest} with an updated verbose logging setting.
* @param verboseLogging if verbose logging should be used
* @return an updated build request
*/
public BuildRequest withVerboseLogging(boolean verboseLogging) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.creator, this.env,
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, verboseLogging);
}
@ -175,6 +189,14 @@ public class BuildRequest {
return this.builder;
}
/**
* Return the run image that should be used, if provided.
* @return the run image
*/
public ImageReference getRunImage() {
return this.runImage;
}
/**
* Return the {@link Creator} the builder should use.
* @return the {@code Creator}

@ -63,15 +63,12 @@ public class Builder {
Image builderImage = pullBuilder(request);
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
StackId stackId = StackId.fromImage(builderImage);
ImageReference runImageReference = getRunImageReference(builderMetadata.getStack());
Image runImage = pullRunImage(request, runImageReference);
assertHasExpectedStackId(runImage, stackId);
request = determineRunImage(request, builderImage, builderMetadata.getStack());
EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getCreator(),
request.getEnv());
this.docker.image().load(builder.getArchive(), UpdateListener.none());
try {
executeLifecycle(request, runImageReference, builder);
executeLifecycle(request, builder);
}
finally {
this.docker.image().remove(builder.getName(), true);
@ -87,29 +84,41 @@ public class Builder {
return builderImage;
}
private ImageReference getRunImageReference(Stack stack) {
private BuildRequest determineRunImage(BuildRequest request, Image builderImage, Stack builderStack)
throws IOException {
if (request.getRunImage() == null) {
ImageReference runImage = getRunImageReferenceForStack(builderStack);
request = request.withRunImage(runImage);
}
Image runImage = pullRunImage(request);
assertStackIdsMatch(runImage, builderImage);
return request;
}
private ImageReference getRunImageReferenceForStack(Stack stack) {
String name = stack.getRunImage().getImage();
Assert.state(StringUtils.hasText(name), "Run image must be specified");
return ImageReference.of(name).inTaggedForm();
Assert.state(StringUtils.hasText(name), "Run image must be specified in the builder image stack");
return ImageReference.of(name).inTaggedOrDigestForm();
}
private Image pullRunImage(BuildRequest request, ImageReference name) throws IOException {
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingRunImage(request, name);
private Image pullRunImage(BuildRequest request) throws IOException {
ImageReference runImage = request.getRunImage();
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingRunImage(request, runImage);
TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer);
Image image = this.docker.image().pull(name, listener);
Image image = this.docker.image().pull(runImage, listener);
this.log.pulledRunImage(request, image);
return image;
}
private void assertHasExpectedStackId(Image image, StackId stackId) {
StackId pulledStackId = StackId.fromImage(image);
Assert.state(pulledStackId.equals(stackId),
"Run image stack '" + pulledStackId + "' does not match builder stack '" + stackId + "'");
private void assertStackIdsMatch(Image runImage, Image builderImage) {
StackId runImageStackId = StackId.fromImage(runImage);
StackId builderImageStackId = StackId.fromImage(builderImage);
Assert.state(runImageStackId.equals(builderImageStackId),
"Run image stack '" + runImageStackId + "' does not match builder stack '" + builderImageStackId + "'");
}
private void executeLifecycle(BuildRequest request, ImageReference runImageReference, EphemeralBuilder builder)
throws IOException {
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, runImageReference, builder)) {
private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException {
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, builder)) {
lifecycle.execute();
}
}

@ -48,8 +48,6 @@ class Lifecycle implements Closeable {
private final BuildRequest request;
private final ImageReference runImageReference;
private final EphemeralBuilder builder;
private final LifecycleVersion lifecycleVersion;
@ -73,15 +71,12 @@ class Lifecycle implements Closeable {
* @param log build output log
* @param docker the Docker API
* @param request the request to process
* @param runImageReference a reference to run image that should be used
* @param builder the ephemeral builder used to run the phases
*/
Lifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReference,
EphemeralBuilder builder) {
Lifecycle(BuildLog log, DockerApi docker, BuildRequest request, EphemeralBuilder builder) {
this.log = log;
this.docker = docker;
this.request = request;
this.runImageReference = runImageReference;
this.builder = builder;
this.lifecycleVersion = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion());
this.platformVersion = ApiVersion.parse(builder.getBuilderMetadata().getLifecycle().getApi().getPlatform());
@ -125,7 +120,7 @@ class Lifecycle implements Closeable {
phase.withLogLevelArg();
phase.withArgs("-app", Directory.APPLICATION);
phase.withArgs("-platform", Directory.PLATFORM);
phase.withArgs("-run-image", this.runImageReference);
phase.withArgs("-run-image", this.request.getRunImage());
phase.withArgs("-layers", Directory.LAYERS);
phase.withArgs("-cache-dir", Directory.CACHE);
phase.withArgs("-launch-cache", Directory.LAUNCH_CACHE);

@ -27,6 +27,7 @@ import org.springframework.util.ObjectUtils;
* A reference to a Docker image of the form {@code "imagename[:tag|@digest]"}.
*
* @author Phillip Webb
* @author Scott Frederick
* @since 2.3.0
* @see ImageName
* @see <a href=
@ -152,7 +153,19 @@ public final class ImageReference {
*/
public ImageReference inTaggedForm() {
Assert.state(this.digest == null, () -> "Image reference '" + this + "' cannot contain a digest");
return new ImageReference(this.name, (this.tag != null) ? this.tag : LATEST, this.digest);
return new ImageReference(this.name, (this.tag != null) ? this.tag : LATEST, null);
}
/**
* Return an {@link ImageReference} containing either a tag or a digest. If neither
* the digest or the tag has been defined then tag {@code latest} is used.
* @return the image reference in tagged or digest form
*/
public ImageReference inTaggedOrDigestForm() {
if (this.digest != null) {
return this;
}
return inTaggedForm();
}
/**

@ -82,7 +82,6 @@ public class BuildRequestTests {
assertThatIllegalArgumentException()
.isThrownBy(() -> BuildRequest.forJarFile(new File(this.tempDir, "missing.jar")))
.withMessage("JarFile must exist");
}
@Test
@ -106,6 +105,21 @@ public class BuildRequestTests {
"docker.io/spring/builder:@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
}
@Test
void withRunImageUpdatesRunImage() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"))
.withRunImage(ImageReference.of("example.com/custom/run-image:latest"));
assertThat(request.getRunImage().toString()).isEqualTo("example.com/custom/run-image:latest");
}
@Test
void withRunImageWhenHasDigestUpdatesRunImage() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")).withRunImage(ImageReference
.of("example.com/custom/run-image:@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"));
assertThat(request.getRunImage().toString()).isEqualTo(
"example.com/custom/run-image:@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
}
@Test
void withCreatorUpdatesCreator() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));

@ -106,6 +106,47 @@ class BuilderTests {
verify(docker.image()).remove(archive.getValue().getTag(), true);
}
@Test
void buildInvokesBuilderWithRunImageInDigestForm() throws Exception {
TestPrintStream out = new TestPrintStream();
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image-with-run-image-digest.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of(
"docker.io/cloudfoundry/run:@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d")),
any())).willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker);
BuildRequest request = getTestRequest();
builder.build(request);
assertThat(out.toString()).contains("Running creator");
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
verify(docker.image()).load(archive.capture(), any());
verify(docker.image()).remove(archive.getValue().getTag(), true);
}
@Test
void buildInvokesBuilderWithRunImageFromRequest() throws Exception {
TestPrintStream out = new TestPrintStream();
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("example.com/custom/run:latest")), any()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker);
BuildRequest request = getTestRequest().withRunImage(ImageReference.of("example.com/custom/run:latest"));
builder.build(request);
assertThat(out.toString()).contains("Running creator");
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
verify(docker.image()).load(archive.capture(), any());
verify(docker.image()).remove(archive.getValue().getTag(), true);
}
@Test
void buildWhenStackIdDoesNotMatchThrowsException() throws Exception {
TestPrintStream out = new TestPrintStream();
@ -175,8 +216,7 @@ class BuilderTests {
private BuildRequest getTestRequest() {
TarArchive content = mock(TarArchive.class);
ImageReference name = ImageReference.of("my-application");
BuildRequest request = BuildRequest.of(name, (owner) -> content);
return request;
return BuildRequest.of(name, (owner) -> content);
}
private Image loadImage(String name) throws IOException {

@ -150,7 +150,7 @@ class LifecycleTests {
private BuildRequest getTestRequest() {
TarArchive content = mock(TarArchive.class);
ImageReference name = ImageReference.of("my-application");
return BuildRequest.of(name, (owner) -> content);
return BuildRequest.of(name, (owner) -> content).withRunImage(ImageReference.of("cloudfoundry/run"));
}
private Lifecycle createLifecycle() throws IOException {
@ -159,8 +159,7 @@ class LifecycleTests {
private Lifecycle createLifecycle(BuildRequest request) throws IOException {
EphemeralBuilder builder = mockEphemeralBuilder();
return new TestLifecycle(BuildLog.to(this.out), this.docker, request, ImageReference.of("cloudfoundry/run"),
builder);
return new TestLifecycle(BuildLog.to(this.out), this.docker, request, builder);
}
private EphemeralBuilder mockEphemeralBuilder() throws IOException {
@ -208,9 +207,8 @@ class LifecycleTests {
static class TestLifecycle extends Lifecycle {
TestLifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReference,
EphemeralBuilder builder) {
super(log, docker, request, runImageReference, builder);
TestLifecycle(BuildLog log, DockerApi docker, BuildRequest request, EphemeralBuilder builder) {
super(log, docker, request, builder);
}
@Override

@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* Tests for {@link ImageReference}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class ImageReferenceTests {
@ -223,6 +224,26 @@ class ImageReferenceTests {
assertThat(reference.inTaggedForm().toString()).isEqualTo("docker.io/library/ubuntu:bionic");
}
@Test
void inTaggedOrDigestFormWhenHasDigestUsesDigest() {
ImageReference reference = ImageReference
.of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
assertThat(reference.inTaggedOrDigestForm().toString()).isEqualTo(
"docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
}
@Test
void inTaggedOrDigestFormWhenHasTagUsesTag() {
ImageReference reference = ImageReference.of("ubuntu:bionic");
assertThat(reference.inTaggedOrDigestForm().toString()).isEqualTo("docker.io/library/ubuntu:bionic");
}
@Test
void inTaggedOrDigestFormWhenHasNoTagOrDigestUsesLatest() {
ImageReference reference = ImageReference.of("ubuntu");
assertThat(reference.inTaggedOrDigestForm().toString()).isEqualTo("docker.io/library/ubuntu:latest");
}
@Test
void equalsAndHashCode() {
ImageReference r1 = ImageReference.of("ubuntu:bionic");

@ -1,7 +1,7 @@
{
"User" : "root",
"Image" : "pack.local/ephemeral-builder",
"Cmd" : [ "/lifecycle/creator", "-app", "/workspace", "-platform", "/platform", "-run-image", "docker.io/cloudfoundry/run", "-layers", "/layers", "-cache-dir", "/cache", "-launch-cache", "/launch-cache", "-daemon", "-skip-restore", "docker.io/library/my-application:latest" ],
"Cmd" : [ "/lifecycle/creator", "-app", "/workspace", "-platform", "/platform", "-run-image", "docker.io/cloudfoundry/run:latest", "-layers", "/layers", "-cache-dir", "/cache", "-launch-cache", "/launch-cache", "-daemon", "-skip-restore", "docker.io/library/my-application:latest" ],
"Labels" : {
"author" : "spring-boot"
},

@ -1,7 +1,7 @@
{
"User" : "root",
"Image" : "pack.local/ephemeral-builder",
"Cmd" : [ "/lifecycle/creator", "-app", "/workspace", "-platform", "/platform", "-run-image", "docker.io/cloudfoundry/run", "-layers", "/layers", "-cache-dir", "/cache", "-launch-cache", "/launch-cache", "-daemon", "docker.io/library/my-application:latest" ],
"Cmd" : [ "/lifecycle/creator", "-app", "/workspace", "-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" ],
"Labels" : {
"author" : "spring-boot"
},

@ -50,6 +50,11 @@ The following table summarizes the available properties and their default values
| Name of the Builder image to use.
| `gcr.io/paketo-buildpacks/builder:base-platform-api-0.3`
| `runImage`
| `--runImage`
| Name of the run image to use.
| No default value, indicating the run image specified in Builder metadata should be used.
| `imageName`
| `--imageName`
| {spring-boot-api}/buildpack/platform/docker/type/ImageReference.html#of-java.lang.String-[Image name] for the generated image.
@ -79,8 +84,8 @@ The following table summarizes the available properties and their default values
[[build-image-example-custom-image-builder]]
==== Custom Image Builder
If you need to customize the builder used to create the image, configure the task as shown in the following example:
==== Custom Image Builder and Run Image
If you need to customize the builder used to create the image or the run image used to launch the built image, configure the task as shown in the following example:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy
@ -94,13 +99,13 @@ include::../gradle/packaging/boot-build-image-builder.gradle[tags=builder]
include::../gradle/packaging/boot-build-image-builder.gradle.kts[tags=builder]
----
This configuration will use a builder image with the name `mine/java-cnb-builder` and the tag `latest`.
This configuration will use a builder image with the name `mine/java-cnb-builder` and the tag `latest`, and the run image named `mine/java-cnb-run` and the tag `latest`.
The builder can be specified on the command line as well, as shown in this example:
The builder and run image can be specified on the command line as well, as shown in this example:
[indent=0]
----
$ gradle bootBuildImage --builder=mine/java-cnb-builder
$ gradle bootBuildImage --builder=mine/java-cnb-builder --runImage=mine/java-cnb-run
----

@ -10,5 +10,6 @@ bootJar {
// tag::builder[]
bootBuildImage {
builder = "mine/java-cnb-builder"
runImage = "mine/java-cnb-run"
}
// end::builder[]

@ -12,5 +12,6 @@ tasks.getByName<BootJar>("bootJar") {
// tag::builder[]
tasks.getByName<BootBuildImage>("bootBuildImage") {
builder = "mine/java-cnb-builder"
runImage = "mine/java-cnb-run"
}
// end::builder[]

@ -61,6 +61,8 @@ public class BootBuildImage extends DefaultTask {
private String builder;
private String runImage;
private Map<String, String> environment = new HashMap<>();
private boolean cleanCache;
@ -133,6 +135,26 @@ public class BootBuildImage extends DefaultTask {
this.builder = builder;
}
/**
* Returns the run image that will be included in the built image. When {@code null},
* the run image bundled with the builder will be used.
* @return the run image
*/
@Input
@Optional
public String getRunImage() {
return this.runImage;
}
/**
* Sets the run image that will be included in the built image.
* @param runImage the run image
*/
@Option(option = "runImage", description = "The name of the run image to use")
public void setRunImage(String runImage) {
this.runImage = runImage;
}
/**
* Returns the environment that will be used when building the image.
* @return the environment
@ -228,6 +250,7 @@ public class BootBuildImage extends DefaultTask {
private BuildRequest customize(BuildRequest request) {
request = customizeBuilder(request);
request = customizeRunImage(request);
request = customizeEnvironment(request);
request = customizeCreator(request);
request = request.withCleanCache(this.cleanCache);
@ -242,6 +265,13 @@ public class BootBuildImage extends DefaultTask {
return request;
}
private BuildRequest customizeRunImage(BuildRequest request) {
if (StringUtils.hasText(this.runImage)) {
return request.withRunImage(ImageReference.of(this.runImage));
}
return request;
}
private BuildRequest customizeEnvironment(BuildRequest request) {
if (this.environment != null && !this.environment.isEmpty()) {
request = request.withEnv(this.environment);

@ -89,13 +89,14 @@ class BootBuildImageIntegrationTests {
}
@TestTemplate
void buildsImageWithCustomBuilder() throws IOException {
void buildsImageWithCustomBuilderAndRunImage() throws IOException {
writeMainClass();
writeLongNameResource();
BuildResult result = this.gradleBuild.build("bootBuildImage");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("example/test-image-custom");
assertThat(result.getOutput()).contains("paketo-buildpacks/builder:full-cf-platform-api-0.3");
assertThat(result.getOutput()).contains("paketo-buildpacks/run:full-cnb-cf");
ImageReference imageReference = ImageReference.of(ImageName.of("example/test-image-custom"));
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
@ -110,10 +111,12 @@ class BootBuildImageIntegrationTests {
writeMainClass();
writeLongNameResource();
BuildResult result = this.gradleBuild.build("bootBuildImage", "--imageName=example/test-image-cmd",
"--builder=gcr.io/paketo-buildpacks/builder:full-cf-platform-api-0.3");
"--builder=gcr.io/paketo-buildpacks/builder:full-cf-platform-api-0.3",
"--runImage=gcr.io/paketo-buildpacks/run:full-cnb-cf");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("example/test-image-cmd");
assertThat(result.getOutput()).contains("paketo-buildpacks/builder:full-cf-platform-api-0.3");
assertThat(result.getOutput()).contains("paketo-buildpacks/run:full-cnb-cf");
ImageReference imageReference = ImageReference.of(ImageName.of("example/test-image-cmd"));
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();

@ -183,4 +183,15 @@ class BootBuildImageTests {
assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("test/builder");
}
@Test
void whenNoRunImageIsConfiguredThenRequestUsesDefaultRunImage() {
assertThat(this.buildImage.createRequest().getRunImage()).isNull();
}
@Test
void whenRunImageIsConfiguredThenRequestUsesSpecifiedRunImage() {
this.buildImage.setRunImage("example.com/test/run:1.0");
assertThat(this.buildImage.createRequest().getRunImage().getName()).isEqualTo("test/run");
}
}

@ -1,12 +0,0 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
bootBuildImage {
imageName = "example/test-image-custom"
builder = "gcr.io/paketo-buildpacks/builder:full-cf-platform-api-0.3"
}

@ -0,0 +1,13 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
bootBuildImage {
imageName = "example/test-image-custom"
builder = "gcr.io/paketo-buildpacks/builder:full-cf-platform-api-0.3"
runImage = "gcr.io/paketo-buildpacks/run:full-cnb-cf"
}

@ -6,6 +6,6 @@ plugins {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
bootBuildImage {
imageName = "example/test-image-name"
}
bootBuildImage {
imageName = "example/test-image-name"
}

@ -6,6 +6,6 @@ plugins {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
bootBuildImage {
environment = ["BP_JVM_VERSION" : "13.9.9"]
}
bootBuildImage {
environment = ["BP_JVM_VERSION": "13.9.9"]
}

@ -43,13 +43,13 @@ The following table shows the environment variables and their values:
|===
| Environment variable | Description
| DOCKER_HOST
| DOCKER_HOST
| URL containing the host and port for the Docker daemon - e.g. `tcp://192.168.99.100:2376`
| DOCKER_TLS_VERIFY
| DOCKER_TLS_VERIFY
| Enable secure HTTPS protocol when set to `1` (optional)
| DOCKER_CERT_PATH
| DOCKER_CERT_PATH
| Path to certificate and key files for HTTPS (required if `DOCKER_TLS_VERIFY=1`, ignored otherwise)
|===
@ -64,7 +64,7 @@ The builder includes multiple {buildpacks-reference}/concepts/components/buildpa
By default, the plugin chooses a builder image.
The name of the generated image is deduced from project properties.
The `image` parameter allows to configure how the builder should operate on the project.
The `image` parameter allows configuration of the builder and how it should operate on the project.
The following table summarizes the available parameters and their default values:
|===
@ -75,6 +75,11 @@ The following table summarizes the available parameters and their default values
| `spring-boot.build-image.builder`
| `gcr.io/paketo-buildpacks/builder:base-platform-api-0.3`
| `runImage`
| Name of the run image to use.
| `spring-boot.build-image.runImage`
| No default value, indicating the run image specified in Builder metadata should be used.
| `name`
| {spring-boot-api}/buildpack/platform/docker/type/ImageReference.html#of-java.lang.String-[Image name] for the generated image.
| `spring-boot.build-image.imageName`
@ -109,7 +114,7 @@ include::goals/build-image.adoc[leveloffset=+1]
[[build-image-example-custom-image-builder]]
==== Custom Image Builder
If you need to customize the builder used to create the image, configure the plugin as shown in the following example:
If you need to customize the builder used to create the image or the run image used to launch the built image, configure the plugin as shown in the following example:
[source,xml,indent=0,subs="verbatim,attributes"]
----
@ -123,6 +128,7 @@ If you need to customize the builder used to create the image, configure the plu
<configuration>
<image>
<builder>mine/java-cnb-builder</builder>
<runImage>mine/java-cnb-run</runImage>
</image>
</configuration>
</plugin>
@ -131,13 +137,13 @@ If you need to customize the builder used to create the image, configure the plu
</project>
----
This configuration will use a builder image with the name `mine/java-cnb-builder` and the tag `latest`.
This configuration will use a builder image with the name `mine/java-cnb-builder` and the tag `latest`, and the run image named `mine/java-cnb-run` and the tag `latest`.
The builder can be specified on the command line as well, as shown in this example:
The builder and run image can be specified on the command line as well, as shown in this example:
[indent=0]
----
$ mvn spring-boot:build-image -Dspring-boot.build-image.builder=mine/java-cnb-builder
$ mvn spring-boot:build-image -Dspring-boot.build-image.builder=mine/java-cnb-builder -Dspring-boot.build-image.runImage=mine/java-cnb-run
----

@ -95,11 +95,12 @@ public class BuildImageTests extends AbstractArchiveIntegrationTests {
.systemProperty("spring-boot.build-image.imageName", "example.com/test/cmd-property-name:v1")
.systemProperty("spring-boot.build-image.builder",
"gcr.io/paketo-buildpacks/builder:full-cf-platform-api-0.3")
.systemProperty("spring-boot.build-image.runImage", "gcr.io/paketo-buildpacks/run:full-cnb-cf")
.execute((project) -> {
assertThat(buildLog(project)).contains("Building image")
.contains("example.com/test/cmd-property-name:v1")
.contains("paketo-buildpacks/builder:full-cf-platform-api-0.3")
.contains("Successfully built image");
.contains("paketo-buildpacks/run:full-cnb-cf").contains("Successfully built image");
ImageReference imageReference = ImageReference.of("example.com/test/cmd-property-name:v1");
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
@ -111,10 +112,11 @@ public class BuildImageTests extends AbstractArchiveIntegrationTests {
}
@TestTemplate
void whenBuildImageIsInvokedWithCustomBuilderImage(MavenBuild mavenBuild) {
void whenBuildImageIsInvokedWithCustomBuilderImageAndRunImage(MavenBuild mavenBuild) {
mavenBuild.project("build-image-custom-builder").goals("package").execute((project) -> {
assertThat(buildLog(project)).contains("Building image")
.contains("paketo-buildpacks/builder:full-cf-platform-api-0.3")
.contains("paketo-buildpacks/run:full-cnb-cf")
.contains("docker.io/library/build-image-v2-builder:0.0.1.BUILD-SNAPSHOT")
.contains("Successfully built image");
ImageReference imageReference = ImageReference

@ -24,6 +24,7 @@
<configuration>
<image>
<builder>gcr.io/paketo-buildpacks/builder:full-cf-platform-api-0.3</builder>
<runImage>gcr.io/paketo-buildpacks/run:full-cnb-cf</runImage>
</image>
</configuration>
</execution>

@ -94,7 +94,7 @@ public class BuildImageMojo extends AbstractPackagerMojo {
private String classifier;
/**
* Image configuration, with `builder`, `name`, `env`, `cleanCache` and
* Image configuration, with `builder`, `runImage`, `name`, `env`, `cleanCache` and
* `verboseLogging` options.
* @since 2.3.0
*/
@ -115,6 +115,14 @@ public class BuildImageMojo extends AbstractPackagerMojo {
@Parameter(property = "spring-boot.build-image.builder", readonly = true)
String imageBuilder;
/**
* Alias for {@link Image#runImage} to support configuration via command-line
* property.
* @since 2.3.1
*/
@Parameter(property = "spring-boot.build-image.runImage", readonly = true)
String runImage;
@Override
public void execute() throws MojoExecutionException {
if (this.project.getPackaging().equals("pom")) {
@ -149,6 +157,9 @@ public class BuildImageMojo extends AbstractPackagerMojo {
if (image.builder == null && this.imageBuilder != null) {
image.setBuilder(this.imageBuilder);
}
if (image.runImage == null && this.runImage != null) {
image.setRunImage(this.runImage);
}
return customize(image.getBuildRequest(this.project.getArtifact(), content));
}

@ -47,6 +47,11 @@ public class Image {
*/
String builder;
/**
* The run image used to launch the built image.
*/
String runImage;
/**
* Environment properties that should be passed to the builder.
*/
@ -70,6 +75,10 @@ public class Image {
this.builder = builder;
}
void setRunImage(String runImage) {
this.runImage = runImage;
}
BuildRequest getBuildRequest(Artifact artifact, Function<Owner, TarArchive> applicationContent) {
return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent));
}
@ -86,6 +95,9 @@ public class Image {
if (StringUtils.hasText(this.builder)) {
request = request.withBuilder(ImageReference.of(this.builder));
}
if (StringUtils.hasText(this.runImage)) {
request = request.withRunImage(ImageReference.of(this.runImage));
}
if (this.env != null && !this.env.isEmpty()) {
request = request.withEnv(this.env);
}

@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.entry;
* Tests for {@link Image}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class ImageTests {
@ -58,6 +59,7 @@ class ImageTests {
BuildRequest request = new Image().getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1-SNAPSHOT");
assertThat(request.getBuilder().toString()).contains("paketo-buildpacks/builder");
assertThat(request.getRunImage()).isNull();
assertThat(request.getEnv()).isEmpty();
assertThat(request.isCleanCache()).isFalse();
assertThat(request.isVerboseLogging()).isFalse();
@ -71,6 +73,14 @@ class ImageTests {
assertThat(request.getBuilder().toString()).isEqualTo("docker.io/springboot/builder:2.2.x");
}
@Test
void getBuildRequestWhenHasRunImageUsesRunImage() {
Image image = new Image();
image.runImage = "springboot/run:latest";
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getRunImage().toString()).isEqualTo("docker.io/springboot/run:latest");
}
@Test
void getBuildRequestWhenHasEnvUsesEnv() {
Image image = new Image();

Loading…
Cancel
Save