Commit 6119d696 authored by Scott Frederick's avatar Scott Frederick

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
parent 025d7aaa
...@@ -46,6 +46,8 @@ public class BuildRequest { ...@@ -46,6 +46,8 @@ public class BuildRequest {
private final ImageReference builder; private final ImageReference builder;
private final ImageReference runImage;
private final Creator creator; private final Creator creator;
private final Map<String, String> env; private final Map<String, String> env;
...@@ -60,6 +62,7 @@ public class BuildRequest { ...@@ -60,6 +62,7 @@ public class BuildRequest {
this.name = name.inTaggedForm(); this.name = name.inTaggedForm();
this.applicationContent = applicationContent; this.applicationContent = applicationContent;
this.builder = DEFAULT_BUILDER; this.builder = DEFAULT_BUILDER;
this.runImage = null;
this.env = Collections.emptyMap(); this.env = Collections.emptyMap();
this.cleanCache = false; this.cleanCache = false;
this.verboseLogging = false; this.verboseLogging = false;
...@@ -67,10 +70,12 @@ public class BuildRequest { ...@@ -67,10 +70,12 @@ public class BuildRequest {
} }
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder, 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.name = name;
this.applicationContent = applicationContent; this.applicationContent = applicationContent;
this.builder = builder; this.builder = builder;
this.runImage = runImage;
this.creator = creator; this.creator = creator;
this.env = env; this.env = env;
this.cleanCache = cleanCache; this.cleanCache = cleanCache;
...@@ -84,20 +89,29 @@ public class BuildRequest { ...@@ -84,20 +89,29 @@ public class BuildRequest {
*/ */
public BuildRequest withBuilder(ImageReference builder) { public BuildRequest withBuilder(ImageReference builder) {
Assert.notNull(builder, "Builder must not be null"); Assert.notNull(builder, "Builder must not be null");
builder = (builder.getDigest() != null) ? builder : builder.inTaggedForm(); return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
return new BuildRequest(this.name, this.applicationContent, builder, this.creator, this.env, this.cleanCache, this.creator, this.env, this.cleanCache, this.verboseLogging);
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 * @param creator the new {@code Creator} to use
* @return an updated build request * @return an updated build request
*/ */
public BuildRequest withCreator(Creator creator) { public BuildRequest withCreator(Creator creator) {
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, creator, this.env, this.cleanCache, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
this.verboseLogging); this.cleanCache, this.verboseLogging);
} }
/** /**
...@@ -111,12 +125,12 @@ public class BuildRequest { ...@@ -111,12 +125,12 @@ public class BuildRequest {
Assert.hasText(value, "Value must not be empty"); Assert.hasText(value, "Value must not be empty");
Map<String, String> env = new LinkedHashMap<>(this.env); Map<String, String> env = new LinkedHashMap<>(this.env);
env.put(name, value); 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); 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 * @param env the additional variables
* @return an updated build request * @return an updated build request
*/ */
...@@ -124,27 +138,27 @@ public class BuildRequest { ...@@ -124,27 +138,27 @@ public class BuildRequest {
Assert.notNull(env, "Env must not be null"); Assert.notNull(env, "Env must not be null");
Map<String, String> updatedEnv = new LinkedHashMap<>(this.env); Map<String, String> updatedEnv = new LinkedHashMap<>(this.env);
updatedEnv.putAll(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); 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 * @param cleanCache if the cache should be cleaned
* @return an updated build request * @return an updated build request
*/ */
public BuildRequest withCleanCache(boolean cleanCache) { public BuildRequest withCleanCache(boolean cleanCache) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.creator, this.env, cleanCache, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.verboseLogging); 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 * @param verboseLogging if verbose logging should be used
* @return an updated build request * @return an updated build request
*/ */
public BuildRequest withVerboseLogging(boolean verboseLogging) { 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); this.cleanCache, verboseLogging);
} }
...@@ -175,6 +189,14 @@ public class BuildRequest { ...@@ -175,6 +189,14 @@ public class BuildRequest {
return this.builder; 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 {@link Creator} the builder should use.
* @return the {@code Creator} * @return the {@code Creator}
......
...@@ -63,15 +63,12 @@ public class Builder { ...@@ -63,15 +63,12 @@ public class Builder {
Image builderImage = pullBuilder(request); Image builderImage = pullBuilder(request);
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage); BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv()); BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
StackId stackId = StackId.fromImage(builderImage); request = determineRunImage(request, builderImage, builderMetadata.getStack());
ImageReference runImageReference = getRunImageReference(builderMetadata.getStack());
Image runImage = pullRunImage(request, runImageReference);
assertHasExpectedStackId(runImage, stackId);
EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getCreator(), EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getCreator(),
request.getEnv()); request.getEnv());
this.docker.image().load(builder.getArchive(), UpdateListener.none()); this.docker.image().load(builder.getArchive(), UpdateListener.none());
try { try {
executeLifecycle(request, runImageReference, builder); executeLifecycle(request, builder);
} }
finally { finally {
this.docker.image().remove(builder.getName(), true); this.docker.image().remove(builder.getName(), true);
...@@ -87,29 +84,41 @@ public class Builder { ...@@ -87,29 +84,41 @@ public class Builder {
return builderImage; 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(); String name = stack.getRunImage().getImage();
Assert.state(StringUtils.hasText(name), "Run image must be specified"); Assert.state(StringUtils.hasText(name), "Run image must be specified in the builder image stack");
return ImageReference.of(name).inTaggedForm(); return ImageReference.of(name).inTaggedOrDigestForm();
} }
private Image pullRunImage(BuildRequest request, ImageReference name) throws IOException { private Image pullRunImage(BuildRequest request) throws IOException {
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingRunImage(request, name); ImageReference runImage = request.getRunImage();
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingRunImage(request, runImage);
TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer); 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); this.log.pulledRunImage(request, image);
return image; return image;
} }
private void assertHasExpectedStackId(Image image, StackId stackId) { private void assertStackIdsMatch(Image runImage, Image builderImage) {
StackId pulledStackId = StackId.fromImage(image); StackId runImageStackId = StackId.fromImage(runImage);
Assert.state(pulledStackId.equals(stackId), StackId builderImageStackId = StackId.fromImage(builderImage);
"Run image stack '" + pulledStackId + "' does not match builder stack '" + stackId + "'"); Assert.state(runImageStackId.equals(builderImageStackId),
"Run image stack '" + runImageStackId + "' does not match builder stack '" + builderImageStackId + "'");
} }
private void executeLifecycle(BuildRequest request, ImageReference runImageReference, EphemeralBuilder builder) private void executeLifecycle(BuildRequest request, EphemeralBuilder builder) throws IOException {
throws IOException { try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, builder)) {
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, runImageReference, builder)) {
lifecycle.execute(); lifecycle.execute();
} }
} }
......
...@@ -48,8 +48,6 @@ class Lifecycle implements Closeable { ...@@ -48,8 +48,6 @@ class Lifecycle implements Closeable {
private final BuildRequest request; private final BuildRequest request;
private final ImageReference runImageReference;
private final EphemeralBuilder builder; private final EphemeralBuilder builder;
private final LifecycleVersion lifecycleVersion; private final LifecycleVersion lifecycleVersion;
...@@ -73,15 +71,12 @@ class Lifecycle implements Closeable { ...@@ -73,15 +71,12 @@ class Lifecycle implements Closeable {
* @param log build output log * @param log build output log
* @param docker the Docker API * @param docker the Docker API
* @param request the request to process * @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 * @param builder the ephemeral builder used to run the phases
*/ */
Lifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReference, Lifecycle(BuildLog log, DockerApi docker, BuildRequest request, EphemeralBuilder builder) {
EphemeralBuilder builder) {
this.log = log; this.log = log;
this.docker = docker; this.docker = docker;
this.request = request; this.request = request;
this.runImageReference = runImageReference;
this.builder = builder; this.builder = builder;
this.lifecycleVersion = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion()); this.lifecycleVersion = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion());
this.platformVersion = ApiVersion.parse(builder.getBuilderMetadata().getLifecycle().getApi().getPlatform()); this.platformVersion = ApiVersion.parse(builder.getBuilderMetadata().getLifecycle().getApi().getPlatform());
...@@ -125,7 +120,7 @@ class Lifecycle implements Closeable { ...@@ -125,7 +120,7 @@ class Lifecycle implements Closeable {
phase.withLogLevelArg(); phase.withLogLevelArg();
phase.withArgs("-app", Directory.APPLICATION); phase.withArgs("-app", Directory.APPLICATION);
phase.withArgs("-platform", Directory.PLATFORM); 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("-layers", Directory.LAYERS);
phase.withArgs("-cache-dir", Directory.CACHE); phase.withArgs("-cache-dir", Directory.CACHE);
phase.withArgs("-launch-cache", Directory.LAUNCH_CACHE); phase.withArgs("-launch-cache", Directory.LAUNCH_CACHE);
......
...@@ -27,6 +27,7 @@ import org.springframework.util.ObjectUtils; ...@@ -27,6 +27,7 @@ import org.springframework.util.ObjectUtils;
* A reference to a Docker image of the form {@code "imagename[:tag|@digest]"}. * A reference to a Docker image of the form {@code "imagename[:tag|@digest]"}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
* @since 2.3.0 * @since 2.3.0
* @see ImageName * @see ImageName
* @see <a href= * @see <a href=
...@@ -152,7 +153,19 @@ public final class ImageReference { ...@@ -152,7 +153,19 @@ public final class ImageReference {
*/ */
public ImageReference inTaggedForm() { public ImageReference inTaggedForm() {
Assert.state(this.digest == null, () -> "Image reference '" + this + "' cannot contain a digest"); 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 { ...@@ -82,7 +82,6 @@ public class BuildRequestTests {
assertThatIllegalArgumentException() assertThatIllegalArgumentException()
.isThrownBy(() -> BuildRequest.forJarFile(new File(this.tempDir, "missing.jar"))) .isThrownBy(() -> BuildRequest.forJarFile(new File(this.tempDir, "missing.jar")))
.withMessage("JarFile must exist"); .withMessage("JarFile must exist");
} }
@Test @Test
...@@ -106,6 +105,21 @@ public class BuildRequestTests { ...@@ -106,6 +105,21 @@ public class BuildRequestTests {
"docker.io/spring/builder:@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); "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 @Test
void withCreatorUpdatesCreator() throws IOException { void withCreatorUpdatesCreator() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
......
...@@ -106,6 +106,47 @@ class BuilderTests { ...@@ -106,6 +106,47 @@ class BuilderTests {
verify(docker.image()).remove(archive.getValue().getTag(), true); 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 @Test
void buildWhenStackIdDoesNotMatchThrowsException() throws Exception { void buildWhenStackIdDoesNotMatchThrowsException() throws Exception {
TestPrintStream out = new TestPrintStream(); TestPrintStream out = new TestPrintStream();
...@@ -175,8 +216,7 @@ class BuilderTests { ...@@ -175,8 +216,7 @@ class BuilderTests {
private BuildRequest getTestRequest() { private BuildRequest getTestRequest() {
TarArchive content = mock(TarArchive.class); TarArchive content = mock(TarArchive.class);
ImageReference name = ImageReference.of("my-application"); ImageReference name = ImageReference.of("my-application");
BuildRequest request = BuildRequest.of(name, (owner) -> content); return BuildRequest.of(name, (owner) -> content);
return request;
} }
private Image loadImage(String name) throws IOException { private Image loadImage(String name) throws IOException {
......
...@@ -150,7 +150,7 @@ class LifecycleTests { ...@@ -150,7 +150,7 @@ class LifecycleTests {
private BuildRequest getTestRequest() { private BuildRequest getTestRequest() {
TarArchive content = mock(TarArchive.class); TarArchive content = mock(TarArchive.class);
ImageReference name = ImageReference.of("my-application"); 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 { private Lifecycle createLifecycle() throws IOException {
...@@ -159,8 +159,7 @@ class LifecycleTests { ...@@ -159,8 +159,7 @@ class LifecycleTests {
private Lifecycle createLifecycle(BuildRequest request) throws IOException { private Lifecycle createLifecycle(BuildRequest request) throws IOException {
EphemeralBuilder builder = mockEphemeralBuilder(); EphemeralBuilder builder = mockEphemeralBuilder();
return new TestLifecycle(BuildLog.to(this.out), this.docker, request, ImageReference.of("cloudfoundry/run"), return new TestLifecycle(BuildLog.to(this.out), this.docker, request, builder);
builder);
} }
private EphemeralBuilder mockEphemeralBuilder() throws IOException { private EphemeralBuilder mockEphemeralBuilder() throws IOException {
...@@ -208,9 +207,8 @@ class LifecycleTests { ...@@ -208,9 +207,8 @@ class LifecycleTests {
static class TestLifecycle extends Lifecycle { static class TestLifecycle extends Lifecycle {
TestLifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReference, TestLifecycle(BuildLog log, DockerApi docker, BuildRequest request, EphemeralBuilder builder) {
EphemeralBuilder builder) { super(log, docker, request, builder);
super(log, docker, request, runImageReference, builder);
} }
@Override @Override
......
...@@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; ...@@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
* Tests for {@link ImageReference}. * Tests for {@link ImageReference}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
*/ */
class ImageReferenceTests { class ImageReferenceTests {
...@@ -223,6 +224,26 @@ class ImageReferenceTests { ...@@ -223,6 +224,26 @@ class ImageReferenceTests {
assertThat(reference.inTaggedForm().toString()).isEqualTo("docker.io/library/ubuntu:bionic"); 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 @Test
void equalsAndHashCode() { void equalsAndHashCode() {
ImageReference r1 = ImageReference.of("ubuntu:bionic"); ImageReference r1 = ImageReference.of("ubuntu:bionic");
......
{ {
"User" : "root", "User" : "root",
"Image" : "pack.local/ephemeral-builder", "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" : { "Labels" : {
"author" : "spring-boot" "author" : "spring-boot"
}, },
......
{ {
"User" : "root", "User" : "root",
"Image" : "pack.local/ephemeral-builder", "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" : { "Labels" : {
"author" : "spring-boot" "author" : "spring-boot"
}, },
......
...@@ -50,6 +50,11 @@ The following table summarizes the available properties and their default values ...@@ -50,6 +50,11 @@ The following table summarizes the available properties and their default values
| Name of the Builder image to use. | Name of the Builder image to use.
| `gcr.io/paketo-buildpacks/builder:base-platform-api-0.3` | `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`
| `--imageName` | `--imageName`
| {spring-boot-api}/buildpack/platform/docker/type/ImageReference.html#of-java.lang.String-[Image name] for the generated image. | {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 ...@@ -79,8 +84,8 @@ The following table summarizes the available properties and their default values
[[build-image-example-custom-image-builder]] [[build-image-example-custom-image-builder]]
==== Custom Image Builder ==== Custom Image Builder and Run Image
If you need to customize the builder used to create the image, configure the task 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 task as shown in the following example:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] [source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy .Groovy
...@@ -94,13 +99,13 @@ include::../gradle/packaging/boot-build-image-builder.gradle[tags=builder] ...@@ -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] 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] [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 { ...@@ -10,5 +10,6 @@ bootJar {
// tag::builder[] // tag::builder[]
bootBuildImage { bootBuildImage {
builder = "mine/java-cnb-builder" builder = "mine/java-cnb-builder"
runImage = "mine/java-cnb-run"
} }
// end::builder[] // end::builder[]
...@@ -12,5 +12,6 @@ tasks.getByName<BootJar>("bootJar") { ...@@ -12,5 +12,6 @@ tasks.getByName<BootJar>("bootJar") {
// tag::builder[] // tag::builder[]
tasks.getByName<BootBuildImage>("bootBuildImage") { tasks.getByName<BootBuildImage>("bootBuildImage") {
builder = "mine/java-cnb-builder" builder = "mine/java-cnb-builder"
runImage = "mine/java-cnb-run"
} }
// end::builder[] // end::builder[]
...@@ -61,6 +61,8 @@ public class BootBuildImage extends DefaultTask { ...@@ -61,6 +61,8 @@ public class BootBuildImage extends DefaultTask {
private String builder; private String builder;
private String runImage;
private Map<String, String> environment = new HashMap<>(); private Map<String, String> environment = new HashMap<>();
private boolean cleanCache; private boolean cleanCache;
...@@ -133,6 +135,26 @@ public class BootBuildImage extends DefaultTask { ...@@ -133,6 +135,26 @@ public class BootBuildImage extends DefaultTask {
this.builder = builder; 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. * Returns the environment that will be used when building the image.
* @return the environment * @return the environment
...@@ -228,6 +250,7 @@ public class BootBuildImage extends DefaultTask { ...@@ -228,6 +250,7 @@ public class BootBuildImage extends DefaultTask {
private BuildRequest customize(BuildRequest request) { private BuildRequest customize(BuildRequest request) {
request = customizeBuilder(request); request = customizeBuilder(request);
request = customizeRunImage(request);
request = customizeEnvironment(request); request = customizeEnvironment(request);
request = customizeCreator(request); request = customizeCreator(request);
request = request.withCleanCache(this.cleanCache); request = request.withCleanCache(this.cleanCache);
...@@ -242,6 +265,13 @@ public class BootBuildImage extends DefaultTask { ...@@ -242,6 +265,13 @@ public class BootBuildImage extends DefaultTask {
return request; 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) { private BuildRequest customizeEnvironment(BuildRequest request) {
if (this.environment != null && !this.environment.isEmpty()) { if (this.environment != null && !this.environment.isEmpty()) {
request = request.withEnv(this.environment); request = request.withEnv(this.environment);
......
...@@ -89,13 +89,14 @@ class BootBuildImageIntegrationTests { ...@@ -89,13 +89,14 @@ class BootBuildImageIntegrationTests {
} }
@TestTemplate @TestTemplate
void buildsImageWithCustomBuilder() throws IOException { void buildsImageWithCustomBuilderAndRunImage() throws IOException {
writeMainClass(); writeMainClass();
writeLongNameResource(); writeLongNameResource();
BuildResult result = this.gradleBuild.build("bootBuildImage"); BuildResult result = this.gradleBuild.build("bootBuildImage");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("example/test-image-custom"); 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/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")); ImageReference imageReference = ImageReference.of(ImageName.of("example/test-image-custom"));
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) { try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start(); container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
...@@ -110,10 +111,12 @@ class BootBuildImageIntegrationTests { ...@@ -110,10 +111,12 @@ class BootBuildImageIntegrationTests {
writeMainClass(); writeMainClass();
writeLongNameResource(); writeLongNameResource();
BuildResult result = this.gradleBuild.build("bootBuildImage", "--imageName=example/test-image-cmd", 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.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("paketo-buildpacks/builder:full-cf-platform-api-0.3"); 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")); ImageReference imageReference = ImageReference.of(ImageName.of("example/test-image-cmd"));
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) { try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start(); container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
......
...@@ -183,4 +183,15 @@ class BootBuildImageTests { ...@@ -183,4 +183,15 @@ class BootBuildImageTests {
assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("test/builder"); 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");
}
} }
...@@ -6,7 +6,8 @@ plugins { ...@@ -6,7 +6,8 @@ plugins {
sourceCompatibility = '1.8' sourceCompatibility = '1.8'
targetCompatibility = '1.8' targetCompatibility = '1.8'
bootBuildImage { bootBuildImage {
imageName = "example/test-image-custom" imageName = "example/test-image-custom"
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"
}
...@@ -6,6 +6,6 @@ plugins { ...@@ -6,6 +6,6 @@ plugins {
sourceCompatibility = '1.8' sourceCompatibility = '1.8'
targetCompatibility = '1.8' targetCompatibility = '1.8'
bootBuildImage { bootBuildImage {
imageName = "example/test-image-name" imageName = "example/test-image-name"
} }
...@@ -6,6 +6,6 @@ plugins { ...@@ -6,6 +6,6 @@ plugins {
sourceCompatibility = '1.8' sourceCompatibility = '1.8'
targetCompatibility = '1.8' targetCompatibility = '1.8'
bootBuildImage { bootBuildImage {
environment = ["BP_JVM_VERSION" : "13.9.9"] environment = ["BP_JVM_VERSION": "13.9.9"]
} }
...@@ -43,13 +43,13 @@ The following table shows the environment variables and their values: ...@@ -43,13 +43,13 @@ The following table shows the environment variables and their values:
|=== |===
| Environment variable | Description | 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` | 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) | 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) | 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 ...@@ -64,7 +64,7 @@ The builder includes multiple {buildpacks-reference}/concepts/components/buildpa
By default, the plugin chooses a builder image. By default, the plugin chooses a builder image.
The name of the generated image is deduced from project properties. 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: 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 ...@@ -75,6 +75,11 @@ The following table summarizes the available parameters and their default values
| `spring-boot.build-image.builder` | `spring-boot.build-image.builder`
| `gcr.io/paketo-buildpacks/builder:base-platform-api-0.3` | `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` | `name`
| {spring-boot-api}/buildpack/platform/docker/type/ImageReference.html#of-java.lang.String-[Image name] for the generated image. | {spring-boot-api}/buildpack/platform/docker/type/ImageReference.html#of-java.lang.String-[Image name] for the generated image.
| `spring-boot.build-image.imageName` | `spring-boot.build-image.imageName`
...@@ -109,7 +114,7 @@ include::goals/build-image.adoc[leveloffset=+1] ...@@ -109,7 +114,7 @@ include::goals/build-image.adoc[leveloffset=+1]
[[build-image-example-custom-image-builder]] [[build-image-example-custom-image-builder]]
==== 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"] [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 ...@@ -123,6 +128,7 @@ If you need to customize the builder used to create the image, configure the plu
<configuration> <configuration>
<image> <image>
<builder>mine/java-cnb-builder</builder> <builder>mine/java-cnb-builder</builder>
<runImage>mine/java-cnb-run</runImage>
</image> </image>
</configuration> </configuration>
</plugin> </plugin>
...@@ -131,13 +137,13 @@ If you need to customize the builder used to create the image, configure the plu ...@@ -131,13 +137,13 @@ If you need to customize the builder used to create the image, configure the plu
</project> </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] [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 { ...@@ -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.imageName", "example.com/test/cmd-property-name:v1")
.systemProperty("spring-boot.build-image.builder", .systemProperty("spring-boot.build-image.builder",
"gcr.io/paketo-buildpacks/builder:full-cf-platform-api-0.3") "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) -> { .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")
.contains("paketo-buildpacks/builder:full-cf-platform-api-0.3") .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"); ImageReference imageReference = ImageReference.of("example.com/test/cmd-property-name:v1");
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) { try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start(); container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
...@@ -111,10 +112,11 @@ public class BuildImageTests extends AbstractArchiveIntegrationTests { ...@@ -111,10 +112,11 @@ public class BuildImageTests extends AbstractArchiveIntegrationTests {
} }
@TestTemplate @TestTemplate
void whenBuildImageIsInvokedWithCustomBuilderImage(MavenBuild mavenBuild) { void whenBuildImageIsInvokedWithCustomBuilderImageAndRunImage(MavenBuild mavenBuild) {
mavenBuild.project("build-image-custom-builder").goals("package").execute((project) -> { mavenBuild.project("build-image-custom-builder").goals("package").execute((project) -> {
assertThat(buildLog(project)).contains("Building image") assertThat(buildLog(project)).contains("Building image")
.contains("paketo-buildpacks/builder:full-cf-platform-api-0.3") .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("docker.io/library/build-image-v2-builder:0.0.1.BUILD-SNAPSHOT")
.contains("Successfully built image"); .contains("Successfully built image");
ImageReference imageReference = ImageReference ImageReference imageReference = ImageReference
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
<configuration> <configuration>
<image> <image>
<builder>gcr.io/paketo-buildpacks/builder:full-cf-platform-api-0.3</builder> <builder>gcr.io/paketo-buildpacks/builder:full-cf-platform-api-0.3</builder>
<runImage>gcr.io/paketo-buildpacks/run:full-cnb-cf</runImage>
</image> </image>
</configuration> </configuration>
</execution> </execution>
......
...@@ -94,7 +94,7 @@ public class BuildImageMojo extends AbstractPackagerMojo { ...@@ -94,7 +94,7 @@ public class BuildImageMojo extends AbstractPackagerMojo {
private String classifier; private String classifier;
/** /**
* Image configuration, with `builder`, `name`, `env`, `cleanCache` and * Image configuration, with `builder`, `runImage`, `name`, `env`, `cleanCache` and
* `verboseLogging` options. * `verboseLogging` options.
* @since 2.3.0 * @since 2.3.0
*/ */
...@@ -115,6 +115,14 @@ public class BuildImageMojo extends AbstractPackagerMojo { ...@@ -115,6 +115,14 @@ public class BuildImageMojo extends AbstractPackagerMojo {
@Parameter(property = "spring-boot.build-image.builder", readonly = true) @Parameter(property = "spring-boot.build-image.builder", readonly = true)
String imageBuilder; 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 @Override
public void execute() throws MojoExecutionException { public void execute() throws MojoExecutionException {
if (this.project.getPackaging().equals("pom")) { if (this.project.getPackaging().equals("pom")) {
...@@ -149,6 +157,9 @@ public class BuildImageMojo extends AbstractPackagerMojo { ...@@ -149,6 +157,9 @@ public class BuildImageMojo extends AbstractPackagerMojo {
if (image.builder == null && this.imageBuilder != null) { if (image.builder == null && this.imageBuilder != null) {
image.setBuilder(this.imageBuilder); image.setBuilder(this.imageBuilder);
} }
if (image.runImage == null && this.runImage != null) {
image.setRunImage(this.runImage);
}
return customize(image.getBuildRequest(this.project.getArtifact(), content)); return customize(image.getBuildRequest(this.project.getArtifact(), content));
} }
......
...@@ -47,6 +47,11 @@ public class Image { ...@@ -47,6 +47,11 @@ public class Image {
*/ */
String builder; String builder;
/**
* The run image used to launch the built image.
*/
String runImage;
/** /**
* Environment properties that should be passed to the builder. * Environment properties that should be passed to the builder.
*/ */
...@@ -70,6 +75,10 @@ public class Image { ...@@ -70,6 +75,10 @@ public class Image {
this.builder = builder; this.builder = builder;
} }
void setRunImage(String runImage) {
this.runImage = runImage;
}
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));
} }
...@@ -86,6 +95,9 @@ public class Image { ...@@ -86,6 +95,9 @@ public class Image {
if (StringUtils.hasText(this.builder)) { if (StringUtils.hasText(this.builder)) {
request = request.withBuilder(ImageReference.of(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()) { if (this.env != null && !this.env.isEmpty()) {
request = request.withEnv(this.env); request = request.withEnv(this.env);
} }
......
...@@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.entry; ...@@ -36,6 +36,7 @@ import static org.assertj.core.api.Assertions.entry;
* Tests for {@link Image}. * Tests for {@link Image}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
*/ */
class ImageTests { class ImageTests {
...@@ -58,6 +59,7 @@ class ImageTests { ...@@ -58,6 +59,7 @@ class ImageTests {
BuildRequest request = new Image().getBuildRequest(createArtifact(), mockApplicationContent()); BuildRequest request = new Image().getBuildRequest(createArtifact(), mockApplicationContent());
assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1-SNAPSHOT"); assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1-SNAPSHOT");
assertThat(request.getBuilder().toString()).contains("paketo-buildpacks/builder"); assertThat(request.getBuilder().toString()).contains("paketo-buildpacks/builder");
assertThat(request.getRunImage()).isNull();
assertThat(request.getEnv()).isEmpty(); assertThat(request.getEnv()).isEmpty();
assertThat(request.isCleanCache()).isFalse(); assertThat(request.isCleanCache()).isFalse();
assertThat(request.isVerboseLogging()).isFalse(); assertThat(request.isVerboseLogging()).isFalse();
...@@ -71,6 +73,14 @@ class ImageTests { ...@@ -71,6 +73,14 @@ class ImageTests {
assertThat(request.getBuilder().toString()).isEqualTo("docker.io/springboot/builder:2.2.x"); 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 @Test
void getBuildRequestWhenHasEnvUsesEnv() { void getBuildRequestWhenHasEnvUsesEnv() {
Image image = new Image(); Image image = new Image();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment