Merge branch '2.3.x'

Closes gh-21933
This commit is contained in:
Scott Frederick
2020-06-15 16:17:39 -05:00
27 changed files with 427 additions and 89 deletions

View File

@@ -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}

View File

@@ -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) {
String name = stack.getRunImage().getImage();
Assert.state(StringUtils.hasText(name), "Run image must be specified");
return ImageReference.of(name).inTaggedForm();
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 Image pullRunImage(BuildRequest request, ImageReference name) throws IOException {
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingRunImage(request, name);
private ImageReference getRunImageReferenceForStack(Stack stack) {
String name = stack.getRunImage().getImage();
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) 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();
}
}

View File

@@ -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);

View File

@@ -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();
}
/**

View File

@@ -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"));

View File

@@ -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 {

View File

@@ -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

View File

@@ -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");

View File

@@ -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"
},

View File

@@ -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"
},