Merge pull request #22736 from anshlykov

* gh-22736:
  Polish "Add pullPolicy option for image building"
  Add pullPolicy option for image building

Closes gh-22736
pull/22946/head
Scott Frederick 4 years ago
commit 8e1768262c

@ -30,6 +30,7 @@ import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick * @author Scott Frederick
* @author Andrey Shlykov
* @since 2.3.0 * @since 2.3.0
*/ */
public abstract class AbstractBuildLog implements BuildLog { public abstract class AbstractBuildLog implements BuildLog {
@ -41,23 +42,37 @@ public abstract class AbstractBuildLog implements BuildLog {
} }
@Override @Override
@Deprecated
public Consumer<TotalProgressEvent> pullingBuilder(BuildRequest request, ImageReference imageReference) { public Consumer<TotalProgressEvent> pullingBuilder(BuildRequest request, ImageReference imageReference) {
return getProgressConsumer(" > Pulling builder image '" + imageReference + "'"); return pullingImage(imageReference, ImageType.BUILDER);
} }
@Override @Override
@Deprecated
public void pulledBuilder(BuildRequest request, Image image) { public void pulledBuilder(BuildRequest request, Image image) {
log(" > Pulled builder image '" + getDigest(image) + "'"); pulledImage(image, ImageType.BUILDER);
} }
@Override @Override
@Deprecated
public Consumer<TotalProgressEvent> pullingRunImage(BuildRequest request, ImageReference imageReference) { public Consumer<TotalProgressEvent> pullingRunImage(BuildRequest request, ImageReference imageReference) {
return getProgressConsumer(" > Pulling run image '" + imageReference + "'"); return pullingImage(imageReference, ImageType.RUNNER);
} }
@Override @Override
@Deprecated
public void pulledRunImage(BuildRequest request, Image image) { public void pulledRunImage(BuildRequest request, Image image) {
log(" > Pulled run image '" + getDigest(image) + "'"); pulledImage(image, ImageType.RUNNER);
}
@Override
public Consumer<TotalProgressEvent> 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 @Override

@ -30,6 +30,7 @@ import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick * @author Scott Frederick
* @author Andrey Shlykov
* @since 2.3.0 * @since 2.3.0
* @see #toSystemOut() * @see #toSystemOut()
*/ */
@ -46,14 +47,19 @@ public interface BuildLog {
* @param request the build request * @param request the build request
* @param imageReference the builder image reference * @param imageReference the builder image reference
* @return a consumer for progress update events * @return a consumer for progress update events
* @deprecated since 2.4.0 in favor of
* {@link #pullingImage(ImageReference, ImageType)}
*/ */
@Deprecated
Consumer<TotalProgressEvent> pullingBuilder(BuildRequest request, ImageReference imageReference); Consumer<TotalProgressEvent> pullingBuilder(BuildRequest request, ImageReference imageReference);
/** /**
* Log that the builder image has been pulled. * Log that the builder image has been pulled.
* @param request the build request * @param request the build request
* @param image the builder image that was pulled * @param image the builder image that was pulled
* @deprecated since 2.4.0 in favor of {@link #pulledImage(Image, ImageType)}
*/ */
@Deprecated
void pulledBuilder(BuildRequest request, Image image); void pulledBuilder(BuildRequest request, Image image);
/** /**
@ -61,16 +67,36 @@ public interface BuildLog {
* @param request the build request * @param request the build request
* @param imageReference the run image reference * @param imageReference the run image reference
* @return a consumer for progress update events * @return a consumer for progress update events
* @deprecated since 2.4.0 in favor of
* {@link #pullingImage(ImageReference, ImageType)}
*/ */
@Deprecated
Consumer<TotalProgressEvent> pullingRunImage(BuildRequest request, ImageReference imageReference); Consumer<TotalProgressEvent> pullingRunImage(BuildRequest request, ImageReference imageReference);
/** /**
* Log that a run image has been pulled. * Log that a run image has been pulled.
* @param request the build request * @param request the build request
* @param image the run image that was pulled * @param image the run image that was pulled
* @deprecated since 2.4.0 in favor of {@link #pulledImage(Image, ImageType)}
*/ */
@Deprecated
void pulledRunImage(BuildRequest request, Image image); void pulledRunImage(BuildRequest request, Image image);
/**
* Log that an image is being pulled.
* @param imageReference the image reference
* @param imageType the image type
* @return a consumer for progress update events
*/
Consumer<TotalProgressEvent> pullingImage(ImageReference imageReference, ImageType imageType);
/**
* Log that an 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. * Log that the lifecycle is executing.
* @param request the build request * @param request the build request

@ -32,6 +32,7 @@ import org.springframework.util.Assert;
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick * @author Scott Frederick
* @author Andrey Shlykov
* @since 2.3.0 * @since 2.3.0
*/ */
public class BuildRequest { public class BuildRequest {
@ -56,6 +57,8 @@ public class BuildRequest {
private final boolean verboseLogging; private final boolean verboseLogging;
private final PullPolicy pullPolicy;
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) { BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
Assert.notNull(name, "Name must not be null"); Assert.notNull(name, "Name must not be null");
Assert.notNull(applicationContent, "ApplicationContent must not be null"); Assert.notNull(applicationContent, "ApplicationContent must not be null");
@ -66,12 +69,13 @@ public class BuildRequest {
this.env = Collections.emptyMap(); this.env = Collections.emptyMap();
this.cleanCache = false; this.cleanCache = false;
this.verboseLogging = false; this.verboseLogging = false;
this.pullPolicy = PullPolicy.ALWAYS;
this.creator = Creator.withVersion(""); this.creator = Creator.withVersion("");
} }
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder, BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache, ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
boolean verboseLogging) { boolean verboseLogging, PullPolicy pullPolicy) {
this.name = name; this.name = name;
this.applicationContent = applicationContent; this.applicationContent = applicationContent;
this.builder = builder; this.builder = builder;
@ -80,6 +84,7 @@ public class BuildRequest {
this.env = env; this.env = env;
this.cleanCache = cleanCache; this.cleanCache = cleanCache;
this.verboseLogging = verboseLogging; this.verboseLogging = verboseLogging;
this.pullPolicy = pullPolicy;
} }
/** /**
@ -90,7 +95,7 @@ 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");
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage, return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging); this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy);
} }
/** /**
@ -100,7 +105,7 @@ public class BuildRequest {
*/ */
public BuildRequest withRunImage(ImageReference runImageName) { public BuildRequest withRunImage(ImageReference runImageName) {
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(), return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
this.creator, this.env, this.cleanCache, this.verboseLogging); this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy);
} }
/** /**
@ -111,7 +116,7 @@ public class BuildRequest {
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, this.runImage, creator, this.env, 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<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.runImage, 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, this.pullPolicy);
} }
/** /**
@ -139,7 +144,7 @@ public class BuildRequest {
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.runImage, 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, this.pullPolicy);
} }
/** /**
@ -149,7 +154,7 @@ public class BuildRequest {
*/ */
public BuildRequest withCleanCache(boolean cleanCache) { public BuildRequest withCleanCache(boolean cleanCache) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
cleanCache, this.verboseLogging); cleanCache, this.verboseLogging, this.pullPolicy);
} }
/** /**
@ -159,7 +164,17 @@ public class BuildRequest {
*/ */
public BuildRequest withVerboseLogging(boolean verboseLogging) { public BuildRequest withVerboseLogging(boolean verboseLogging) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env, return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, verboseLogging); this.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 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. * Factory method to create a new {@link BuildRequest} from a JAR file.
* @param jarFile the source jar file * @param jarFile the source jar file

@ -35,6 +35,7 @@ import org.springframework.util.StringUtils;
* *
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick * @author Scott Frederick
* @author Andrey Shlykov
* @since 2.3.0 * @since 2.3.0
*/ */
public class Builder { public class Builder {
@ -60,7 +61,7 @@ public class Builder {
public void build(BuildRequest request) throws DockerEngineException, IOException { public void build(BuildRequest request) throws DockerEngineException, IOException {
Assert.notNull(request, "Request must not be null"); Assert.notNull(request, "Request must not be null");
this.log.start(request); this.log.start(request);
Image builderImage = pullBuilder(request); Image builderImage = getImage(request, ImageType.BUILDER);
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage); BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv()); BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
request = determineRunImage(request, builderImage, builderMetadata.getStack()); 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<TotalProgressEvent> 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) private BuildRequest determineRunImage(BuildRequest request, Image builderImage, Stack builderStack)
throws IOException { throws IOException {
if (request.getRunImage() == null) { if (request.getRunImage() == null) {
ImageReference runImage = getRunImageReferenceForStack(builderStack); ImageReference runImage = getRunImageReferenceForStack(builderStack);
request = request.withRunImage(runImage); request = request.withRunImage(runImage);
} }
Image runImage = pullRunImage(request); Image runImage = getImage(request, ImageType.RUNNER);
assertStackIdsMatch(runImage, builderImage); assertStackIdsMatch(runImage, builderImage);
return request; return request;
} }
@ -101,12 +93,31 @@ public class Builder {
return ImageReference.of(name).inTaggedOrDigestForm(); return ImageReference.of(name).inTaggedOrDigestForm();
} }
private Image pullRunImage(BuildRequest request) throws IOException { private Image getImage(BuildRequest request, ImageType imageType) throws IOException {
ImageReference runImage = request.getRunImage(); ImageReference imageReference = (imageType == ImageType.BUILDER) ? request.getBuilder() : request.getRunImage();
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingRunImage(request, runImage);
if (request.getPullPolicy() == PullPolicy.ALWAYS) {
return pullImage(imageReference, imageType);
}
try {
return this.docker.image().inspect(imageReference);
}
catch (DockerEngineException exception) {
if (request.getPullPolicy() == PullPolicy.IF_NOT_PRESENT && exception.getStatusCode() == 404) {
return pullImage(imageReference, imageType);
}
else {
throw exception;
}
}
}
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingImage(reference, imageType);
TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer); TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer);
Image image = this.docker.image().pull(runImage, listener); Image image = this.docker.image().pull(reference, listener);
this.log.pulledRunImage(request, image); this.log.pulledImage(image, imageType);
return image; return image;
} }

@ -0,0 +1,46 @@
/*
* 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
*/
enum ImageType {
/**
* Builder image.
*/
BUILDER("builder image"),
/**
* Run image.
*/
RUNNER("run image");
private final String description;
ImageType(String description) {
this.description = description;
}
String getDescription() {
return this.description;
}
}

@ -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 from the registry.
*/
ALWAYS,
/**
* Never pull the image from the registry.
*/
NEVER,
/**
* Pull the image from the registry only if it does not exist locally.
*/
IF_NOT_PRESENT
}

@ -158,10 +158,7 @@ public class DockerApi {
listener.onUpdate(event); listener.onUpdate(event);
}); });
} }
URI imageUri = buildUrl("/images/" + reference.withDigest(digestCapture.getCapturedDigest()) + "/json"); return inspect(reference.withDigest(digestCapture.getCapturedDigest()));
try (Response response = http().get(imageUri)) {
return Image.of(response.getContent());
}
} }
finally { finally {
listener.onFinish(); listener.onFinish();
@ -202,6 +199,20 @@ public class DockerApi {
http().delete(uri); 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());
}
}
} }
/** /**

@ -38,7 +38,7 @@ public class DockerEngineException extends RuntimeException {
private final Message responseMessage; 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) { Message responseMessage) {
super(buildMessage(host, uri, statusCode, reasonPhrase, errors, responseMessage)); super(buildMessage(host, uri, statusCode, reasonPhrase, errors, responseMessage));
this.statusCode = statusCode; this.statusCode = statusCode;

@ -19,6 +19,7 @@ package org.springframework.boot.buildpack.platform.build;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.PrintStream; import java.io.PrintStream;
import java.net.URI;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; 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.ImageApi;
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener; 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.ContainerReference;
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus; import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
import org.springframework.boot.buildpack.platform.docker.type.Image; 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.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
/** /**
@ -147,6 +151,86 @@ class BuilderTests {
verify(docker.image()).remove(archive.getValue().getTag(), true); 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<ImageArchive> 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<ImageArchive> 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<ImageArchive> 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 @Test
void buildWhenStackIdDoesNotMatchThrowsException() throws Exception { void buildWhenStackIdDoesNotMatchThrowsException() throws Exception {
TestPrintStream out = new TestPrintStream(); TestPrintStream out = new TestPrintStream();

@ -57,12 +57,13 @@ class PrintStreamBuildLogTests {
given(runImage.getDigests()).willReturn(Collections.singletonList("00000002")); given(runImage.getDigests()).willReturn(Collections.singletonList("00000002"));
given(request.getName()).willReturn(name); given(request.getName()).willReturn(name);
log.start(request); log.start(request);
Consumer<TotalProgressEvent> pullBuildImageConsumer = log.pullingBuilder(request, builderImageReference); Consumer<TotalProgressEvent> pullBuildImageConsumer = log.pullingImage(builderImageReference,
ImageType.BUILDER);
pullBuildImageConsumer.accept(new TotalProgressEvent(100)); pullBuildImageConsumer.accept(new TotalProgressEvent(100));
log.pulledBuilder(request, builderImage); log.pulledImage(builderImage, ImageType.BUILDER);
Consumer<TotalProgressEvent> pullRunImageConsumer = log.pullingRunImage(request, runImageReference); Consumer<TotalProgressEvent> pullRunImageConsumer = log.pullingImage(runImageReference, ImageType.RUNNER);
pullRunImageConsumer.accept(new TotalProgressEvent(100)); pullRunImageConsumer.accept(new TotalProgressEvent(100));
log.pulledRunImage(request, runImage); log.pulledImage(runImage, ImageType.RUNNER);
log.executingLifecycle(request, LifecycleVersion.parse("0.5"), VolumeName.of("pack-abc.cache")); log.executingLifecycle(request, LifecycleVersion.parse("0.5"), VolumeName.of("pack-abc.cache"));
Consumer<LogUpdateEvent> phase1Consumer = log.runningPhase(request, "alphabet"); Consumer<LogUpdateEvent> phase1Consumer = log.runningPhase(request, "alphabet");
phase1Consumer.accept(mockLogEvent("one")); phase1Consumer.accept(mockLogEvent("one"));

@ -217,6 +217,21 @@ class DockerApiTests {
verify(http()).delete(removeUri); 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 @Nested

@ -61,6 +61,12 @@ The following table summarizes the available properties and their default values
| {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.
| `docker.io/library/${project.artifactId}:${project.version}` | `docker.io/library/${project.artifactId}:${project.version}`
| `pullPolicy`
| `--pullPolicy`
| {spring-boot-api}/buildpack/platform/build/PullPolicy.html[Policy] used to determine when to pull the builder and run images from the registry.
Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`.
| `ALWAYS`
| `environment` | `environment`
| |
| Environment variables that should be passed to the builder. | Environment variables that should be passed to the builder.

@ -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.BuildRequest;
import org.springframework.boot.buildpack.platform.build.Builder; import org.springframework.boot.buildpack.platform.build.Builder;
import org.springframework.boot.buildpack.platform.build.Creator; 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.transport.DockerEngineException;
import org.springframework.boot.buildpack.platform.docker.type.ImageName; import org.springframework.boot.buildpack.platform.docker.type.ImageName;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
@ -69,6 +70,8 @@ public class BootBuildImage extends DefaultTask {
private boolean verboseLogging; private boolean verboseLogging;
private PullPolicy pullPolicy;
public BootBuildImage() { public BootBuildImage() {
this.jar = getProject().getObjects().fileProperty(); this.jar = getProject().getObjects().fileProperty();
this.targetJavaVersion = getProject().getObjects().property(JavaVersion.class); this.targetJavaVersion = getProject().getObjects().property(JavaVersion.class);
@ -224,6 +227,25 @@ public class BootBuildImage extends DefaultTask {
this.verboseLogging = verboseLogging; 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 @TaskAction
void buildImage() throws DockerEngineException, IOException { void buildImage() throws DockerEngineException, IOException {
Builder builder = new Builder(); Builder builder = new Builder();
@ -255,6 +277,7 @@ public class BootBuildImage extends DefaultTask {
request = customizeCreator(request); request = customizeCreator(request);
request = request.withCleanCache(this.cleanCache); request = request.withCleanCache(this.cleanCache);
request = request.withVerboseLogging(this.verboseLogging); request = request.withVerboseLogging(this.verboseLogging);
request = customizePullPolicy(request);
return request; return request;
} }
@ -290,6 +313,13 @@ public class BootBuildImage extends DefaultTask {
return request; return request;
} }
private BuildRequest customizePullPolicy(BuildRequest request) {
if (this.pullPolicy != null) {
request = request.withPullPolicy(this.pullPolicy);
}
return request;
}
private String translateTargetJavaVersion() { private String translateTargetJavaVersion() {
return this.targetJavaVersion.get().getMajorVersion() + ".*"; return this.targetJavaVersion.get().getMajorVersion() + ".*";
} }

@ -126,6 +126,31 @@ class BootBuildImageIntegrationTests {
} }
} }
@TestTemplate
void buildsImageWithPullPolicy() throws IOException {
writeMainClass();
writeLongNameResource();
String projectName = this.gradleBuild.getProjectDir().getName();
ImageReference imageReference = ImageReference.of(ImageName.of(projectName));
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=ALWAYS");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).contains("Pulled builder image").contains("Pulled run image");
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
}
result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(result.getOutput()).doesNotContain("Pulled builder image").doesNotContain("Pulled run image");
try (GenericContainer<?> container = new GenericContainer<>(imageReference.toString())) {
container.waitingFor(Wait.forLogMessage("Launched\\n", 1)).start();
}
finally {
new DockerApi().image().remove(imageReference, false);
}
}
@TestTemplate @TestTemplate
void failsWithLaunchScript() { void failsWithLaunchScript() {
writeMainClass(); writeMainClass();

@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.BuildRequest;
import org.springframework.boot.buildpack.platform.build.PullPolicy;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -35,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Scott Frederick * @author Scott Frederick
* @author Andrey Shlykov
*/ */
class BootBuildImageTests { class BootBuildImageTests {
@ -194,4 +196,15 @@ class BootBuildImageTests {
assertThat(this.buildImage.createRequest().getRunImage().getName()).isEqualTo("test/run"); assertThat(this.buildImage.createRequest().getRunImage().getName()).isEqualTo("test/run");
} }
@Test
void whenUsingDefaultConfigurationThenRequestHasAlwaysPullPolicy() {
assertThat(this.buildImage.createRequest().getPullPolicy()).isEqualTo(PullPolicy.ALWAYS);
}
@Test
void whenPullPolicyIsConfiguredThenRequestHasPullPolicy() {
this.buildImage.setPullPolicy(PullPolicy.NEVER);
assertThat(this.buildImage.createRequest().getPullPolicy()).isEqualTo(PullPolicy.NEVER);
}
} }

@ -85,6 +85,12 @@ The following table summarizes the available parameters and their default values
| `spring-boot.build-image.imageName` | `spring-boot.build-image.imageName`
| `docker.io/library/${project.artifactId}:${project.version}` | `docker.io/library/${project.artifactId}:${project.version}`
| `pullPolicy`
| {spring-boot-api}/buildpack/platform/build/PullPolicy.html[Policy] used to determine when to pull the builder and run images from the registry.
Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`.
| `spring-boot.build-image.pullPolicy`
| `ALWAYS`
| `env` | `env`
| Environment variables that should be passed to the builder. | Environment variables that should be passed to the builder.
| |
@ -101,7 +107,7 @@ The following table summarizes the available parameters and their default values
| `false` | `false`
|=== |===
For more details, see <<build-image-example-custom-image-builder,custom image builder>> and <<build-image-example-custom-image-name,custom image name>>. For more details, see <<build-image-examples,examples>>.
include::goals/build-image.adoc[leveloffset=+1] include::goals/build-image.adoc[leveloffset=+1]

@ -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.BuildRequest;
import org.springframework.boot.buildpack.platform.build.Builder; import org.springframework.boot.buildpack.platform.build.Builder;
import org.springframework.boot.buildpack.platform.build.Creator; 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.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive; 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) @Parameter(property = "spring-boot.build-image.runImage", readonly = true)
String runImage; 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 @Override
public void execute() throws MojoExecutionException { public void execute() throws MojoExecutionException {
if (this.project.getPackaging().equals("pom")) { if (this.project.getPackaging().equals("pom")) {
@ -160,6 +168,9 @@ public class BuildImageMojo extends AbstractPackagerMojo {
if (image.runImage == null && this.runImage != null) { if (image.runImage == null && this.runImage != null) {
image.setRunImage(this.runImage); image.setRunImage(this.runImage);
} }
if (image.pullPolicy == null && this.pullPolicy != null) {
image.setPullPolicy(this.pullPolicy);
}
return customize(image.getBuildRequest(this.project.getArtifact(), content)); return customize(image.getBuildRequest(this.project.getArtifact(), content));
} }

@ -22,6 +22,7 @@ import java.util.function.Function;
import org.apache.maven.artifact.Artifact; import org.apache.maven.artifact.Artifact;
import org.springframework.boot.buildpack.platform.build.BuildRequest; 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.ImageName;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.Owner;
@ -67,6 +68,11 @@ public class Image {
*/ */
boolean verboseLogging; boolean verboseLogging;
/**
* If images should be pulled from a remote repository during image build.
*/
PullPolicy pullPolicy;
void setName(String name) { void setName(String name) {
this.name = name; this.name = name;
} }
@ -79,6 +85,10 @@ public class Image {
this.runImage = runImage; this.runImage = runImage;
} }
public void setPullPolicy(PullPolicy pullPolicy) {
this.pullPolicy = pullPolicy;
}
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));
} }
@ -103,6 +113,9 @@ public class Image {
} }
request = request.withCleanCache(this.cleanCache); request = request.withCleanCache(this.cleanCache);
request = request.withVerboseLogging(this.verboseLogging); request = request.withVerboseLogging(this.verboseLogging);
if (this.pullPolicy != null) {
request = request.withPullPolicy(this.pullPolicy);
}
return request; return request;
} }

@ -26,6 +26,7 @@ import org.apache.maven.artifact.versioning.VersionRange;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.build.BuildRequest; 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.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.boot.buildpack.platform.io.TarArchive;
@ -63,6 +64,7 @@ class ImageTests {
assertThat(request.getEnv()).isEmpty(); assertThat(request.getEnv()).isEmpty();
assertThat(request.isCleanCache()).isFalse(); assertThat(request.isCleanCache()).isFalse();
assertThat(request.isVerboseLogging()).isFalse(); assertThat(request.isVerboseLogging()).isFalse();
assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.ALWAYS);
} }
@Test @Test
@ -105,6 +107,14 @@ class ImageTests {
assertThat(request.isVerboseLogging()).isTrue(); 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() { private Artifact createArtifact() {
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile", return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
"jar", null, new DefaultArtifactHandler()); "jar", null, new DefaultArtifactHandler());

Loading…
Cancel
Save