diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java index 7a78d5cbeb..e668917a70 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java @@ -42,22 +42,32 @@ public abstract class AbstractBuildLog implements BuildLog { @Override public Consumer pullingBuilder(BuildRequest request, ImageReference imageReference) { - return getProgressConsumer(" > Pulling builder image '" + imageReference + "'"); + return pullingImage(imageReference, ImageType.BUILDER); } @Override public void pulledBuilder(BuildRequest request, Image image) { - log(" > Pulled builder image '" + getDigest(image) + "'"); + pulledImage(image, ImageType.BUILDER); } @Override public Consumer pullingRunImage(BuildRequest request, ImageReference imageReference) { - return getProgressConsumer(" > Pulling run image '" + imageReference + "'"); + return pullingImage(imageReference, ImageType.RUNNER); } @Override public void pulledRunImage(BuildRequest request, Image image) { - log(" > Pulled run image '" + getDigest(image) + "'"); + pulledImage(image, ImageType.RUNNER); + } + + @Override + public Consumer pullingImage(ImageReference imageReference, ImageType imageType) { + return getProgressConsumer(String.format(" > Pulling %s '%s'", imageType.getDescription(), imageReference)); + } + + @Override + public void pulledImage(Image image, ImageType imageType) { + log(String.format(" > Pulled %s '%s'", imageType.getDescription(), getDigest(image))); } @Override diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java index cb82832b7a..da13f1eb22 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java @@ -71,6 +71,21 @@ public interface BuildLog { */ void pulledRunImage(BuildRequest request, Image image); + /** + * Log that the image is being pulled. + * @param imageReference the image reference + * @param imageType the image type + * @return a consumer for progress update events + */ + Consumer pullingImage(ImageReference imageReference, ImageType imageType); + + /** + * Log that the image has been pulled. + * @param image the builder image that was pulled + * @param imageType the image type that was pulled + */ + void pulledImage(Image image, ImageType imageType); + /** * Log that the lifecycle is executing. * @param request the build request diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index e8a5f1867e..7f65984b53 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -32,6 +32,7 @@ import org.springframework.util.Assert; * * @author Phillip Webb * @author Scott Frederick + * @author Andrey Shlykov * @since 2.3.0 */ public class BuildRequest { @@ -56,6 +57,8 @@ public class BuildRequest { private final boolean verboseLogging; + private final PullPolicy pullPolicy; + BuildRequest(ImageReference name, Function applicationContent) { Assert.notNull(name, "Name must not be null"); Assert.notNull(applicationContent, "ApplicationContent must not be null"); @@ -66,12 +69,13 @@ public class BuildRequest { this.env = Collections.emptyMap(); this.cleanCache = false; this.verboseLogging = false; + this.pullPolicy = PullPolicy.ALWAYS; this.creator = Creator.withVersion(""); } BuildRequest(ImageReference name, Function applicationContent, ImageReference builder, ImageReference runImage, Creator creator, Map env, boolean cleanCache, - boolean verboseLogging) { + boolean verboseLogging, PullPolicy pullPolicy) { this.name = name; this.applicationContent = applicationContent; this.builder = builder; @@ -80,6 +84,7 @@ public class BuildRequest { this.env = env; this.cleanCache = cleanCache; this.verboseLogging = verboseLogging; + this.pullPolicy = pullPolicy; } /** @@ -90,7 +95,7 @@ public class BuildRequest { public BuildRequest withBuilder(ImageReference builder) { Assert.notNull(builder, "Builder must not be null"); return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, - this.creator, this.env, this.cleanCache, this.verboseLogging); + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy); } /** @@ -100,7 +105,7 @@ public class BuildRequest { */ public BuildRequest withRunImage(ImageReference runImageName) { return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), - this.creator, this.env, this.cleanCache, this.verboseLogging); + this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy); } /** @@ -111,7 +116,7 @@ public class BuildRequest { public BuildRequest withCreator(Creator creator) { Assert.notNull(creator, "Creator must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env, - this.cleanCache, this.verboseLogging); + this.cleanCache, this.verboseLogging, this.pullPolicy); } /** @@ -126,7 +131,7 @@ public class BuildRequest { Map env = new LinkedHashMap<>(this.env); env.put(name, value); 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, this.pullPolicy); } /** @@ -139,7 +144,7 @@ public class BuildRequest { Map updatedEnv = new LinkedHashMap<>(this.env); updatedEnv.putAll(env); 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, this.pullPolicy); } /** @@ -149,7 +154,7 @@ public class BuildRequest { */ public BuildRequest withCleanCache(boolean cleanCache) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - cleanCache, this.verboseLogging); + cleanCache, this.verboseLogging, this.pullPolicy); } /** @@ -159,7 +164,17 @@ public class BuildRequest { */ public BuildRequest withVerboseLogging(boolean verboseLogging) { return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, - this.cleanCache, verboseLogging); + this.cleanCache, verboseLogging, this.pullPolicy); + } + + /** + * Return a new {@link BuildRequest} with the updated image pull policy. + * @param pullPolicy image pull policy {@link PullPolicy} + * @return an updated build request + */ + public BuildRequest withPullPolicy(PullPolicy pullPolicy) { + return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, + this.cleanCache, this.verboseLogging, pullPolicy); } /** @@ -229,6 +244,14 @@ public class BuildRequest { return this.verboseLogging; } + /** + * Return the image {@link PullPolicy} that the builder should use. + * @return image pull policy + */ + public PullPolicy getPullPolicy() { + return this.pullPolicy; + } + /** * Factory method to create a new {@link BuildRequest} from a JAR file. * @param jarFile the source jar file diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index cf83dddd15..3737d2e885 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -35,6 +35,7 @@ import org.springframework.util.StringUtils; * * @author Phillip Webb * @author Scott Frederick + * @author Andrey Shlykov * @since 2.3.0 */ public class Builder { @@ -60,7 +61,7 @@ public class Builder { public void build(BuildRequest request) throws DockerEngineException, IOException { Assert.notNull(request, "Request must not be null"); this.log.start(request); - Image builderImage = pullBuilder(request); + Image builderImage = getImage(request, ImageType.BUILDER); BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage); BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv()); request = determineRunImage(request, builderImage, builderMetadata.getStack()); @@ -75,22 +76,13 @@ public class Builder { } } - private Image pullBuilder(BuildRequest request) throws IOException { - ImageReference builderImageReference = request.getBuilder(); - Consumer progressConsumer = this.log.pullingBuilder(request, builderImageReference); - TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer); - Image builderImage = this.docker.image().pull(builderImageReference, listener); - this.log.pulledBuilder(request, builderImage); - return builderImage; - } - 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); + Image runImage = getImage(request, ImageType.RUNNER); assertStackIdsMatch(runImage, builderImage); return request; } @@ -101,12 +93,35 @@ public class Builder { return ImageReference.of(name).inTaggedOrDigestForm(); } - private Image pullRunImage(BuildRequest request) throws IOException { - ImageReference runImage = request.getRunImage(); - Consumer progressConsumer = this.log.pullingRunImage(request, runImage); + private Image getImage(BuildRequest request, ImageType imageType) throws IOException { + ImageReference imageReference = (imageType == ImageType.BUILDER) ? request.getBuilder() : request.getRunImage(); + + Image image; + if (request.getPullPolicy() != PullPolicy.ALWAYS) { + try { + image = this.docker.image().inspect(imageReference); + } + catch (DockerEngineException exception) { + if (request.getPullPolicy() == PullPolicy.IF_NOT_PRESENT && exception.getStatusCode() == 404) { + image = pullImage(imageReference, imageType); + } + else { + throw exception; + } + } + } + else { + image = pullImage(imageReference, imageType); + } + + return image; + } + + private Image pullImage(ImageReference reference, ImageType imageType) throws IOException { + Consumer progressConsumer = this.log.pullingImage(reference, imageType); TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer); - Image image = this.docker.image().pull(runImage, listener); - this.log.pulledRunImage(request, image); + Image image = this.docker.image().pull(reference, listener); + this.log.pulledImage(image, imageType); return image; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageType.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageType.java new file mode 100644 index 0000000000..68713962b8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageType.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +/** + * Image types. + * + * @author Andrey Shlykov + * @since 2.4.0 + */ +public enum ImageType { + + /** + * Builder image. + */ + BUILDER("builder image"), + + /** + * Run image. + */ + RUNNER("run image"); + + private final String description; + + ImageType(String description) { + this.description = description; + } + + public String getDescription() { + return this.description; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/PullPolicy.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/PullPolicy.java new file mode 100644 index 0000000000..6969687b4b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/PullPolicy.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.build; + +/** + * Image pull policy. + * + * @author Andrey Shlykov + * @since 2.4.0 + */ +public enum PullPolicy { + + /** + * Always pull the image. + */ + ALWAYS, + + /** + * Never pull the image. + */ + NEVER, + + /** + * Pull the image if it does not already exist in registry. + */ + IF_NOT_PRESENT + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index 8986c06b9e..053aeeb6e8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -158,10 +158,7 @@ public class DockerApi { listener.onUpdate(event); }); } - URI imageUri = buildUrl("/images/" + reference.withDigest(digestCapture.getCapturedDigest()) + "/json"); - try (Response response = http().get(imageUri)) { - return Image.of(response.getContent()); - } + return inspect(reference.withDigest(digestCapture.getCapturedDigest())); } finally { listener.onFinish(); @@ -202,6 +199,20 @@ public class DockerApi { http().delete(uri); } + /** + * Inspect an image. + * @param reference the image reference + * @return the image from the local repository + * @throws IOException on IO error + */ + public Image inspect(ImageReference reference) throws IOException { + Assert.notNull(reference, "Reference must not be null"); + URI imageUri = buildUrl("/images/" + reference + "/json"); + try (Response response = http().get(imageUri)) { + return Image.of(response.getContent()); + } + } + } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java index 075b5314bf..b7780f8605 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java @@ -38,7 +38,7 @@ public class DockerEngineException extends RuntimeException { private final Message responseMessage; - DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors, + public DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors, Message responseMessage) { super(buildMessage(host, uri, statusCode, reasonPhrase, errors, responseMessage)); this.statusCode = statusCode; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java index 99789bb4ce..dd9669f0ee 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java @@ -19,6 +19,7 @@ package org.springframework.boot.buildpack.platform.build; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; +import java.net.URI; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; @@ -29,6 +30,7 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi; import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener; +import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; import org.springframework.boot.buildpack.platform.docker.type.Image; @@ -44,6 +46,8 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; /** @@ -147,6 +151,86 @@ class BuilderTests { verify(docker.image()).remove(archive.getValue().getTag(), true); } + @Test + void buildInvokesBuilderWithNeverPullPolicy() 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("docker.io/cloudfoundry/run:base-cnb")), any())) + .willAnswer(withPulledImage(runImage)); + given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)))) + .willReturn(builderImage); + given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))) + .willReturn(runImage); + Builder builder = new Builder(BuildLog.to(out), docker); + BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.NEVER); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + verify(docker.image()).load(archive.capture(), any()); + verify(docker.image()).remove(archive.getValue().getTag(), true); + verify(docker.image(), never()).pull(any(), any()); + verify(docker.image(), times(2)).inspect(any()); + } + + @Test + void buildInvokesBuilderWithAlwaysPullPolicy() 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("docker.io/cloudfoundry/run:base-cnb")), any())) + .willAnswer(withPulledImage(runImage)); + given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)))) + .willReturn(builderImage); + given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))) + .willReturn(runImage); + Builder builder = new Builder(BuildLog.to(out), docker); + BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.ALWAYS); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + verify(docker.image()).load(archive.capture(), any()); + verify(docker.image()).remove(archive.getValue().getTag(), true); + verify(docker.image(), times(2)).pull(any(), any()); + verify(docker.image(), never()).inspect(any()); + } + + @Test + void buildInvokesBuilderWithIfNotPresentPullPolicy() 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("docker.io/cloudfoundry/run:base-cnb")), any())) + .willAnswer(withPulledImage(runImage)); + given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)))).willThrow( + new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null)) + .willReturn(builderImage); + given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))).willThrow( + new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null)) + .willReturn(runImage); + Builder builder = new Builder(BuildLog.to(out), docker); + BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.IF_NOT_PRESENT); + builder.build(request); + assertThat(out.toString()).contains("Running creator"); + assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + verify(docker.image()).load(archive.capture(), any()); + verify(docker.image()).remove(archive.getValue().getTag(), true); + verify(docker.image(), times(2)).inspect(any()); + verify(docker.image(), times(2)).pull(any(), any()); + } + @Test void buildWhenStackIdDoesNotMatchThrowsException() throws Exception { TestPrintStream out = new TestPrintStream(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index c256afeb1c..dc72c38622 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -217,6 +217,21 @@ class DockerApiTests { verify(http()).delete(removeUri); } + @Test + void inspectWhenReferenceIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> this.api.inspect(null)) + .withMessage("Reference must not be null"); + } + + @Test + void inspectInspectImage() throws Exception { + ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base"); + URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder:base/json"); + given(http().get(imageUri)).willReturn(responseOf("type/image.json")); + Image image = this.api.inspect(reference); + assertThat(image.getLayers()).hasSize(46); + } + } @Nested diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 787ba46cb7..dfdc211f2d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -34,6 +34,7 @@ import org.gradle.api.tasks.options.Option; import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.Builder; import org.springframework.boot.buildpack.platform.build.Creator; +import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.ImageName; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; @@ -69,6 +70,8 @@ public class BootBuildImage extends DefaultTask { private boolean verboseLogging; + private PullPolicy pullPolicy; + public BootBuildImage() { this.jar = getProject().getObjects().fileProperty(); this.targetJavaVersion = getProject().getObjects().property(JavaVersion.class); @@ -224,6 +227,25 @@ public class BootBuildImage extends DefaultTask { this.verboseLogging = verboseLogging; } + /** + * Returns image pull policy that will be used when building the image. + * @return whether images should be pulled + */ + @Input + @Optional + public PullPolicy getPullPolicy() { + return this.pullPolicy; + } + + /** + * Sets image pull policy that will be used when building the image. + * @param pullPolicy image pull policy {@link PullPolicy} + */ + @Option(option = "pullPolicy", description = "The image pull policy") + public void setPullPolicy(PullPolicy pullPolicy) { + this.pullPolicy = pullPolicy; + } + @TaskAction void buildImage() throws DockerEngineException, IOException { Builder builder = new Builder(); @@ -255,6 +277,7 @@ public class BootBuildImage extends DefaultTask { request = customizeCreator(request); request = request.withCleanCache(this.cleanCache); request = request.withVerboseLogging(this.verboseLogging); + request = customizePullPolicy(request); return request; } @@ -290,6 +313,13 @@ public class BootBuildImage extends DefaultTask { return request; } + private BuildRequest customizePullPolicy(BuildRequest request) { + if (this.pullPolicy != null) { + request = request.withPullPolicy(this.pullPolicy); + } + return request; + } + private String translateTargetJavaVersion() { return this.targetJavaVersion.get().getMajorVersion() + ".*"; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index 485e32e08d..6ec40ca0b5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.PullPolicy; import static org.assertj.core.api.Assertions.assertThat; @@ -194,4 +195,15 @@ class BootBuildImageTests { assertThat(this.buildImage.createRequest().getRunImage().getName()).isEqualTo("test/run"); } + @Test + void whenUsingDefaultConfigurationThenRequestHasNoPullDisabled() { + assertThat(this.buildImage.createRequest().getPullPolicy()).isEqualTo(PullPolicy.ALWAYS); + } + + @Test + void whenNoPullIsEnabledThenRequestHasNoPullEnabled() { + this.buildImage.setPullPolicy(PullPolicy.NEVER); + assertThat(this.buildImage.createRequest().getPullPolicy()).isEqualTo(PullPolicy.NEVER); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java index fab1b6660a..d633979318 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java @@ -42,6 +42,7 @@ import org.springframework.boot.buildpack.platform.build.BuildLog; import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.Builder; import org.springframework.boot.buildpack.platform.build.Creator; +import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.TarArchive; @@ -123,6 +124,13 @@ public class BuildImageMojo extends AbstractPackagerMojo { @Parameter(property = "spring-boot.build-image.runImage", readonly = true) String runImage; + /** + * Alias for {@link Image#pullPolicy} to support configuration via command-line + * property. + */ + @Parameter(property = "spring-boot.build-image.pullPolicy", readonly = true) + PullPolicy pullPolicy; + @Override public void execute() throws MojoExecutionException { if (this.project.getPackaging().equals("pom")) { @@ -160,6 +168,9 @@ public class BuildImageMojo extends AbstractPackagerMojo { if (image.runImage == null && this.runImage != null) { image.setRunImage(this.runImage); } + if (image.pullPolicy == null && this.pullPolicy != null) { + image.setPullPolicy(this.pullPolicy); + } return customize(image.getBuildRequest(this.project.getArtifact(), content)); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java index ada2452471..71ce39d19f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -22,6 +22,7 @@ import java.util.function.Function; import org.apache.maven.artifact.Artifact; import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.type.ImageName; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.Owner; @@ -67,6 +68,11 @@ public class Image { */ boolean verboseLogging; + /** + * If images should be pulled from a remote repository during image build. + */ + PullPolicy pullPolicy; + void setName(String name) { this.name = name; } @@ -79,6 +85,10 @@ public class Image { this.runImage = runImage; } + public void setPullPolicy(PullPolicy pullPolicy) { + this.pullPolicy = pullPolicy; + } + BuildRequest getBuildRequest(Artifact artifact, Function applicationContent) { return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent)); } @@ -103,6 +113,9 @@ public class Image { } request = request.withCleanCache(this.cleanCache); request = request.withVerboseLogging(this.verboseLogging); + if (this.pullPolicy != null) { + request = request.withPullPolicy(this.pullPolicy); + } return request; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index 292cc8f7e7..cdf5163001 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -26,6 +26,7 @@ import org.apache.maven.artifact.versioning.VersionRange; import org.junit.jupiter.api.Test; import org.springframework.boot.buildpack.platform.build.BuildRequest; +import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.TarArchive; @@ -63,6 +64,7 @@ class ImageTests { assertThat(request.getEnv()).isEmpty(); assertThat(request.isCleanCache()).isFalse(); assertThat(request.isVerboseLogging()).isFalse(); + assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.ALWAYS); } @Test @@ -105,6 +107,14 @@ class ImageTests { assertThat(request.isVerboseLogging()).isTrue(); } + @Test + void getBuildRequestWhenHasPullPolicyUsesPullPolicy() { + Image image = new Image(); + image.setPullPolicy(PullPolicy.NEVER); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.NEVER); + } + private Artifact createArtifact() { return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile", "jar", null, new DefaultArtifactHandler());