Merge pull request #27613 from ceccone
* pr/27613: Polish "Add option to create tags for a built image" Add option to create tags for a built image Closes gh-27613
This commit is contained in:
@@ -31,6 +31,7 @@ import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Andrey Shlykov
|
||||
* @author Rafael Ceccone
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public abstract class AbstractBuildLog implements BuildLog {
|
||||
@@ -89,6 +90,12 @@ public abstract class AbstractBuildLog implements BuildLog {
|
||||
log();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void taggedImage(ImageReference tag) {
|
||||
log("Successfully created image tag '" + tag + "'");
|
||||
log();
|
||||
}
|
||||
|
||||
private String getDigest(Image image) {
|
||||
List<String> digests = image.getDigests();
|
||||
return (digests.isEmpty() ? "" : digests.get(0));
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.springframework.boot.buildpack.platform.docker.type.VolumeName;
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Andrey Shlykov
|
||||
* @author Rafael Ceccone
|
||||
* @since 2.3.0
|
||||
* @see #toSystemOut()
|
||||
*/
|
||||
@@ -99,6 +100,12 @@ public interface BuildLog {
|
||||
*/
|
||||
void executedLifecycle(BuildRequest request);
|
||||
|
||||
/**
|
||||
* Log that a tag has been created.
|
||||
* @param tag the tag reference
|
||||
*/
|
||||
void taggedImage(ImageReference tag);
|
||||
|
||||
/**
|
||||
* Factory method that returns a {@link BuildLog} the outputs to {@link System#out}.
|
||||
* @return a build log instance that logs to system out
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.util.Assert;
|
||||
* @author Scott Frederick
|
||||
* @author Andrey Shlykov
|
||||
* @author Jeroen Meijer
|
||||
* @author Rafael Ceccone
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class BuildRequest {
|
||||
@@ -71,6 +72,8 @@ public class BuildRequest {
|
||||
|
||||
private final String network;
|
||||
|
||||
private final List<ImageReference> tags;
|
||||
|
||||
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
Assert.notNull(applicationContent, "ApplicationContent must not be null");
|
||||
@@ -87,12 +90,13 @@ public class BuildRequest {
|
||||
this.buildpacks = Collections.emptyList();
|
||||
this.bindings = Collections.emptyList();
|
||||
this.network = null;
|
||||
this.tags = Collections.emptyList();
|
||||
}
|
||||
|
||||
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
|
||||
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
|
||||
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks,
|
||||
List<Binding> bindings, String network) {
|
||||
List<Binding> bindings, String network, List<ImageReference> tags) {
|
||||
this.name = name;
|
||||
this.applicationContent = applicationContent;
|
||||
this.builder = builder;
|
||||
@@ -106,6 +110,7 @@ public class BuildRequest {
|
||||
this.buildpacks = buildpacks;
|
||||
this.bindings = bindings;
|
||||
this.network = network;
|
||||
this.tags = tags;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +122,7 @@ public class BuildRequest {
|
||||
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.pullPolicy, this.publish,
|
||||
this.buildpacks, this.bindings, this.network);
|
||||
this.buildpacks, this.bindings, this.network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +133,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.pullPolicy, this.publish,
|
||||
this.buildpacks, this.bindings, this.network);
|
||||
this.buildpacks, this.bindings, this.network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +145,7 @@ public class BuildRequest {
|
||||
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.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
this.network);
|
||||
this.network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,7 +161,7 @@ public class BuildRequest {
|
||||
env.put(name, value);
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
|
||||
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
|
||||
this.buildpacks, this.bindings, this.network);
|
||||
this.buildpacks, this.bindings, this.network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,7 +175,7 @@ public class BuildRequest {
|
||||
updatedEnv.putAll(env);
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
|
||||
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy,
|
||||
this.publish, this.buildpacks, this.bindings, this.network);
|
||||
this.publish, this.buildpacks, this.bindings, this.network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,7 +186,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, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
this.network);
|
||||
this.network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,7 +197,7 @@ 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.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
this.network);
|
||||
this.network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +208,7 @@ public class BuildRequest {
|
||||
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, this.publish, this.buildpacks, this.bindings,
|
||||
this.network);
|
||||
this.network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,7 +219,7 @@ public class BuildRequest {
|
||||
public BuildRequest withPublish(boolean publish) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings,
|
||||
this.network);
|
||||
this.network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -238,7 +243,7 @@ public class BuildRequest {
|
||||
Assert.notNull(buildpacks, "Buildpacks must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings,
|
||||
this.network);
|
||||
this.network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,7 +267,7 @@ public class BuildRequest {
|
||||
Assert.notNull(bindings, "Bindings must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings,
|
||||
this.network);
|
||||
this.network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,7 +279,29 @@ public class BuildRequest {
|
||||
public BuildRequest withNetwork(String network) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
network);
|
||||
network, this.tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link BuildRequest} with updated tags.
|
||||
* @param tags a collection of tags to be created for the built image
|
||||
* @return an updated build request
|
||||
*/
|
||||
public BuildRequest withTags(ImageReference... tags) {
|
||||
Assert.notEmpty(tags, "Tags must not be empty");
|
||||
return withTags(Arrays.asList(tags));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link BuildRequest} with updated tags.
|
||||
* @param tags a collection of tags to be created for the built image
|
||||
* @return an updated build request
|
||||
*/
|
||||
public BuildRequest withTags(List<ImageReference> tags) {
|
||||
Assert.notNull(tags, "Tags must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
|
||||
this.network, tags);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -386,6 +413,14 @@ public class BuildRequest {
|
||||
return this.network;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the collection of tags that should be created.
|
||||
* @return the tags
|
||||
*/
|
||||
public List<ImageReference> getTags() {
|
||||
return this.tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link BuildRequest} from a JAR file.
|
||||
* @param jarFile the source jar file
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Andrey Shlykov
|
||||
* @author Rafael Ceccone
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class Builder {
|
||||
@@ -110,8 +111,9 @@ public class Builder {
|
||||
this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none());
|
||||
try {
|
||||
executeLifecycle(request, ephemeralBuilder);
|
||||
tagImage(request.getName(), request.getTags());
|
||||
if (request.isPublish()) {
|
||||
pushImage(request.getName());
|
||||
pushImages(request.getName(), request.getTags());
|
||||
}
|
||||
}
|
||||
finally {
|
||||
@@ -150,6 +152,20 @@ public class Builder {
|
||||
}
|
||||
}
|
||||
|
||||
private void tagImage(ImageReference sourceReference, List<ImageReference> tags) throws IOException {
|
||||
for (ImageReference tag : tags) {
|
||||
this.docker.image().tag(sourceReference, tag);
|
||||
this.log.taggedImage(tag);
|
||||
}
|
||||
}
|
||||
|
||||
private void pushImages(ImageReference name, List<ImageReference> tags) throws IOException {
|
||||
pushImage(name);
|
||||
for (ImageReference tag : tags) {
|
||||
pushImage(tag);
|
||||
}
|
||||
}
|
||||
|
||||
private void pushImage(ImageReference reference) throws IOException {
|
||||
Consumer<TotalProgressEvent> progressConsumer = this.log.pushingImage(reference);
|
||||
TotalProgressPushListener listener = new TotalProgressPushListener(progressConsumer);
|
||||
|
||||
@@ -52,6 +52,7 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Rafael Ceccone
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class DockerApi {
|
||||
@@ -283,7 +284,7 @@ public class DockerApi {
|
||||
Assert.notNull(reference, "Reference must not be null");
|
||||
Collection<String> params = force ? FORCE_PARAMS : Collections.emptySet();
|
||||
URI uri = buildUrl("/images/" + reference, params);
|
||||
http().delete(uri);
|
||||
http().delete(uri).close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -300,6 +301,13 @@ public class DockerApi {
|
||||
}
|
||||
}
|
||||
|
||||
public void tag(ImageReference sourceReference, ImageReference targetReference) throws IOException {
|
||||
Assert.notNull(sourceReference, "SourceReference must not be null");
|
||||
Assert.notNull(targetReference, "TargetReference must not be null");
|
||||
URI uri = buildUrl("/images/" + sourceReference + "/tag", "repo", targetReference.toString());
|
||||
http().post(uri).close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -348,7 +356,7 @@ public class DockerApi {
|
||||
public void start(ContainerReference reference) throws IOException {
|
||||
Assert.notNull(reference, "Reference must not be null");
|
||||
URI uri = buildUrl("/containers/" + reference + "/start");
|
||||
http().post(uri);
|
||||
http().post(uri).close();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -396,7 +404,7 @@ public class DockerApi {
|
||||
Assert.notNull(reference, "Reference must not be null");
|
||||
Collection<String> params = force ? FORCE_PARAMS : Collections.emptySet();
|
||||
URI uri = buildUrl("/containers/" + reference, params);
|
||||
http().delete(uri);
|
||||
http().delete(uri).close();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -419,7 +427,7 @@ public class DockerApi {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
Collection<String> params = force ? FORCE_PARAMS : Collections.emptySet();
|
||||
URI uri = buildUrl("/volumes/" + name, params);
|
||||
http().delete(uri);
|
||||
http().delete(uri).close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import static org.assertj.core.api.Assertions.entry;
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Jeroen Meijer
|
||||
* @author Rafael Ceccone
|
||||
*/
|
||||
class BuildRequestTests {
|
||||
|
||||
@@ -206,6 +207,25 @@ class BuildRequestTests {
|
||||
assertThat(request.getNetwork()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withTagsAddsTags() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
|
||||
BuildRequest witTags = request.withTags(ImageReference.of("docker.io/library/my-app:latest"),
|
||||
ImageReference.of("example.com/custom/my-app:0.0.1"),
|
||||
ImageReference.of("example.com/custom/my-app:latest"));
|
||||
assertThat(request.getTags()).isEmpty();
|
||||
assertThat(witTags.getTags()).containsExactly(ImageReference.of("docker.io/library/my-app:latest"),
|
||||
ImageReference.of("example.com/custom/my-app:0.0.1"),
|
||||
ImageReference.of("example.com/custom/my-app:latest"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withTagsWhenTagsIsNullThrowsException() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> request.withTags((List<ImageReference>) null))
|
||||
.withMessage("Tags must not be null");
|
||||
}
|
||||
|
||||
private void hasExpectedJarContent(TarArchive archive) {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
@@ -58,6 +58,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Rafael Ceccone
|
||||
*/
|
||||
class BuilderTests {
|
||||
|
||||
@@ -276,6 +277,65 @@ class BuilderTests {
|
||||
verify(docker.image(), times(2)).pull(any(), any(), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildInvokesBuilderWithTags() 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(), isNull()))
|
||||
.willAnswer(withPulledImage(builderImage));
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
|
||||
.willAnswer(withPulledImage(runImage));
|
||||
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||
BuildRequest request = getTestRequest().withTags(ImageReference.of("my-application:1.2.3"));
|
||||
builder.build(request);
|
||||
assertThat(out.toString()).contains("Running creator");
|
||||
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
|
||||
assertThat(out.toString()).contains("Successfully created image tag 'docker.io/library/my-application:1.2.3'");
|
||||
verify(docker.image()).tag(eq(request.getName()), eq(ImageReference.of("my-application:1.2.3")));
|
||||
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
|
||||
verify(docker.image()).load(archive.capture(), any());
|
||||
verify(docker.image()).remove(archive.getValue().getTag(), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildInvokesBuilderWithTagsAndPublishesImageAndTags() throws Exception {
|
||||
TestPrintStream out = new TestPrintStream();
|
||||
DockerApi docker = mockDockerApi();
|
||||
Image builderImage = loadImage("image.json");
|
||||
Image runImage = loadImage("run-image.json");
|
||||
DockerConfiguration dockerConfiguration = new DockerConfiguration()
|
||||
.withBuilderRegistryTokenAuthentication("builder token")
|
||||
.withPublishRegistryTokenAuthentication("publish token");
|
||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(),
|
||||
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
|
||||
.willAnswer(withPulledImage(builderImage));
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
|
||||
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
|
||||
.willAnswer(withPulledImage(runImage));
|
||||
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
|
||||
BuildRequest request = getTestRequest().withPublish(true).withTags(ImageReference.of("my-application:1.2.3"));
|
||||
builder.build(request);
|
||||
assertThat(out.toString()).contains("Running creator");
|
||||
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
|
||||
assertThat(out.toString()).contains("Successfully created image tag 'docker.io/library/my-application:1.2.3'");
|
||||
|
||||
verify(docker.image()).pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(),
|
||||
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
|
||||
verify(docker.image()).pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
|
||||
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
|
||||
verify(docker.image()).push(eq(request.getName()), any(),
|
||||
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
|
||||
verify(docker.image()).tag(eq(request.getName()), eq(ImageReference.of("my-application:1.2.3")));
|
||||
verify(docker.image()).push(eq(ImageReference.of("my-application:1.2.3")), any(),
|
||||
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
|
||||
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
|
||||
verify(docker.image()).load(archive.capture(), any());
|
||||
verify(docker.image()).remove(archive.getValue().getTag(), true);
|
||||
verifyNoMoreInteractions(docker.image());
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenStackIdDoesNotMatchThrowsException() throws Exception {
|
||||
TestPrintStream out = new TestPrintStream();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* Copyright 2012-2021 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.
|
||||
@@ -40,6 +40,7 @@ import static org.mockito.Mockito.mock;
|
||||
* Tests for {@link PrintStreamBuildLog}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Rafael Ceccone
|
||||
*/
|
||||
class PrintStreamBuildLogTests {
|
||||
|
||||
@@ -56,6 +57,8 @@ class PrintStreamBuildLogTests {
|
||||
Image runImage = mock(Image.class);
|
||||
given(runImage.getDigests()).willReturn(Collections.singletonList("00000002"));
|
||||
given(request.getName()).willReturn(name);
|
||||
ImageReference tag = ImageReference.of("my-app:1.0");
|
||||
given(request.getTags()).willReturn(Collections.singletonList(tag));
|
||||
log.start(request);
|
||||
Consumer<TotalProgressEvent> pullBuildImageConsumer = log.pullingImage(builderImageReference,
|
||||
ImageType.BUILDER);
|
||||
@@ -73,6 +76,7 @@ class PrintStreamBuildLogTests {
|
||||
phase2Consumer.accept(mockLogEvent("spring"));
|
||||
phase2Consumer.accept(mockLogEvent("boot"));
|
||||
log.executedLifecycle(request);
|
||||
log.taggedImage(tag);
|
||||
String expected = FileCopyUtils.copyToString(new InputStreamReader(
|
||||
getClass().getResourceAsStream("print-stream-build-log.txt"), StandardCharsets.UTF_8));
|
||||
assertThat(out.toString()).isEqualToIgnoringNewLines(expected);
|
||||
|
||||
@@ -71,6 +71,7 @@ import static org.mockito.Mockito.verify;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Rafael Ceccone
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DockerApiTests {
|
||||
@@ -348,6 +349,30 @@ class DockerApiTests {
|
||||
.containsExactly("etc/", "etc/apt/", "etc/apt/sources.list");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tagWhenReferenceIsNullThrowsException() {
|
||||
ImageReference tag = ImageReference.of("localhost:5000/ubuntu");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.tag(null, tag))
|
||||
.withMessage("SourceReference must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tagWhenTargetIsNullThrowsException() {
|
||||
ImageReference reference = ImageReference.of("localhost:5000/ubuntu");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.tag(reference, null))
|
||||
.withMessage("TargetReference must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void tagTagsImage() throws Exception {
|
||||
ImageReference sourceReference = ImageReference.of("localhost:5000/ubuntu");
|
||||
ImageReference targetReference = ImageReference.of("localhost:5000/ubuntu:tagged");
|
||||
URI tagURI = new URI(IMAGES_URL + "/localhost:5000/ubuntu/tag?repo=localhost%3A5000%2Fubuntu%3Atagged");
|
||||
given(http().post(tagURI)).willReturn(emptyResponse());
|
||||
this.api.tag(sourceReference, targetReference);
|
||||
verify(http()).post(tagURI);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
||||
@@ -17,3 +17,5 @@ Building image 'docker.io/library/my-app:latest'
|
||||
[basket] boot
|
||||
|
||||
Successfully built image 'docker.io/library/my-app:latest'
|
||||
|
||||
Successfully created image tag 'docker.io/library/my-app:1.0'
|
||||
|
||||
@@ -176,6 +176,11 @@ The value supplied will be passed unvalidated to Docker when creating the builde
|
||||
| Whether to publish the generated image to a Docker registry.
|
||||
| `false`
|
||||
|
||||
| `tags`
|
||||
|
|
||||
| A list of one or more additional tags to apply to the generated image.
|
||||
|
|
||||
|
||||
|===
|
||||
|
||||
NOTE: The plugin detects the target Java compatibility of the project using the JavaPlugin's `targetCompatibility` property.
|
||||
|
||||
@@ -58,6 +58,7 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @author Rafael Ceccone
|
||||
* @author Jeroen Meijer
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@@ -95,6 +96,8 @@ public class BootBuildImage extends DefaultTask {
|
||||
|
||||
private String network;
|
||||
|
||||
private final ListProperty<String> tags;
|
||||
|
||||
private final DockerSpec docker = new DockerSpec();
|
||||
|
||||
public BootBuildImage() {
|
||||
@@ -106,6 +109,7 @@ public class BootBuildImage extends DefaultTask {
|
||||
this.projectVersion.set(getProject().provider(() -> project.getVersion().toString()));
|
||||
this.buildpacks = getProject().getObjects().listProperty(String.class);
|
||||
this.bindings = getProject().getObjects().listProperty(String.class);
|
||||
this.tags = getProject().getObjects().listProperty(String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -379,6 +383,40 @@ public class BootBuildImage extends DefaultTask {
|
||||
this.bindings.addAll(bindings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tags that will be created for the built image.
|
||||
* @return the tags
|
||||
*/
|
||||
@Input
|
||||
@Optional
|
||||
public List<String> getTags() {
|
||||
return this.tags.getOrNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the tags that will be created for the built image.
|
||||
* @param tags the tags
|
||||
*/
|
||||
public void setTags(List<String> tags) {
|
||||
this.tags.set(tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the tags that will be created for the built image.
|
||||
* @param tag the tag
|
||||
*/
|
||||
public void tag(String tag) {
|
||||
this.tags.add(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add entries to the tags that will be created for the built image.
|
||||
* @param tags the tags
|
||||
*/
|
||||
public void tags(List<String> tags) {
|
||||
this.tags.addAll(tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the network the build container will connect to.
|
||||
* @return the network
|
||||
@@ -460,6 +498,7 @@ public class BootBuildImage extends DefaultTask {
|
||||
request = customizePublish(request);
|
||||
request = customizeBuildpacks(request);
|
||||
request = customizeBindings(request);
|
||||
request = customizeTags(request);
|
||||
request = request.withNetwork(this.network);
|
||||
return request;
|
||||
}
|
||||
@@ -529,6 +568,14 @@ public class BootBuildImage extends DefaultTask {
|
||||
return request;
|
||||
}
|
||||
|
||||
private BuildRequest customizeTags(BuildRequest request) {
|
||||
List<String> tags = this.tags.getOrNull();
|
||||
if (tags != null && !tags.isEmpty()) {
|
||||
return request.withTags(tags.stream().map(ImageReference::of).collect(Collectors.toList()));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private String translateTargetJavaVersion() {
|
||||
return this.targetJavaVersion.get().getMajorVersion() + ".*";
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
|
||||
import org.springframework.boot.buildpack.platform.docker.DockerApi;
|
||||
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.FilePermissions;
|
||||
import org.springframework.boot.gradle.junit.GradleCompatibility;
|
||||
@@ -54,6 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
* @author Rafael Ceccone
|
||||
*/
|
||||
@GradleCompatibility(configurationCache = true)
|
||||
@DisabledIfDockerUnavailable
|
||||
@@ -235,6 +235,21 @@ class BootBuildImageIntegrationTests {
|
||||
removeImage(projectName);
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void buildsImageWithTag() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.build("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
String projectName = this.gradleBuild.getProjectDir().getName();
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||
assertThat(result.getOutput()).contains("docker.io/library/" + projectName);
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack building");
|
||||
assertThat(result.getOutput()).contains("---> Test Info buildpack done");
|
||||
assertThat(result.getOutput()).contains("example.com/myapp:latest");
|
||||
removeImage(projectName);
|
||||
removeImage("example.com/myapp:latest");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void buildsImageWithLaunchScript() throws IOException {
|
||||
writeMainClass();
|
||||
@@ -300,6 +315,16 @@ class BootBuildImageIntegrationTests {
|
||||
assertThat(result.getOutput()).contains("'urn:cnb:builder:example/does-not-exist:0.0.1' not found in builder");
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWithInvalidTag() throws IOException {
|
||||
writeMainClass();
|
||||
writeLongNameResource();
|
||||
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--pullPolicy=IF_NOT_PRESENT");
|
||||
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED);
|
||||
assertThat(result.getOutput()).containsPattern("Unable to parse image reference")
|
||||
.containsPattern("example/Invalid-Tag-Name");
|
||||
}
|
||||
|
||||
private void writeMainClass() throws IOException {
|
||||
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
|
||||
examplePackage.mkdirs();
|
||||
@@ -423,7 +448,7 @@ class BootBuildImageIntegrationTests {
|
||||
}
|
||||
|
||||
private void removeImage(String name) throws IOException {
|
||||
ImageReference imageReference = ImageReference.of(ImageName.of(name));
|
||||
ImageReference imageReference = ImageReference.of(name);
|
||||
new DockerApi().image().remove(imageReference, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.springframework.boot.buildpack.platform.build.BuildRequest;
|
||||
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
|
||||
import org.springframework.boot.buildpack.platform.build.PullPolicy;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.Binding;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||
import org.springframework.boot.gradle.junit.GradleProjectBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -43,6 +44,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
* @author Scott Frederick
|
||||
* @author Andrey Shlykov
|
||||
* @author Jeroen Meijer
|
||||
* @author Rafael Ceccone
|
||||
*/
|
||||
class BootBuildImageTests {
|
||||
|
||||
@@ -288,4 +290,34 @@ class BootBuildImageTests {
|
||||
assertThat(this.buildImage.createRequest().getNetwork()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenNoTagsAreConfiguredThenRequestHasNoTags() {
|
||||
assertThat(this.buildImage.createRequest().getTags()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenTagsAreConfiguredThenRequestHasTags() {
|
||||
this.buildImage.setTags(
|
||||
Arrays.asList("my-app:latest", "example.com/my-app:0.0.1-SNAPSHOT", "example.com/my-app:latest"));
|
||||
assertThat(this.buildImage.createRequest().getTags()).containsExactly(ImageReference.of("my-app:latest"),
|
||||
ImageReference.of("example.com/my-app:0.0.1-SNAPSHOT"), ImageReference.of("example.com/my-app:latest"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenEntriesAreAddedToTagsThenRequestHasTags() {
|
||||
this.buildImage
|
||||
.tags(Arrays.asList("my-app:latest", "example.com/my-app:0.0.1-SNAPSHOT", "example.com/my-app:latest"));
|
||||
assertThat(this.buildImage.createRequest().getTags()).containsExactly(ImageReference.of("my-app:latest"),
|
||||
ImageReference.of("example.com/my-app:0.0.1-SNAPSHOT"), ImageReference.of("example.com/my-app:latest"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenIndividualEntriesAreAddedToTagsThenRequestHasTags() {
|
||||
this.buildImage.tag("my-app:latest");
|
||||
this.buildImage.tag("example.com/my-app:0.0.1-SNAPSHOT");
|
||||
this.buildImage.tag("example.com/my-app:latest");
|
||||
assertThat(this.buildImage.createRequest().getTags()).containsExactly(ImageReference.of("my-app:latest"),
|
||||
ImageReference.of("example.com/my-app:0.0.1-SNAPSHOT"), ImageReference.of("example.com/my-app:latest"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
bootBuildImage {
|
||||
builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1"
|
||||
tags = [ "example.com/myapp:latest" ]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '{version}'
|
||||
}
|
||||
|
||||
sourceCompatibility = '1.8'
|
||||
targetCompatibility = '1.8'
|
||||
|
||||
bootBuildImage {
|
||||
builder = "projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1"
|
||||
tags = [ "example/Invalid-Tag-Name" ]
|
||||
}
|
||||
@@ -183,6 +183,10 @@ The value supplied will be passed unvalidated to Docker when creating the builde
|
||||
| Whether to publish the generated image to a Docker registry.
|
||||
| `false`
|
||||
|
||||
| `tags`
|
||||
| One or more additional tags to apply to the generated image.
|
||||
|
|
||||
|
||||
|===
|
||||
|
||||
NOTE: The plugin detects the target Java compatibility of the project using the compiler's plugin configuration or the `maven.compiler.target` property.
|
||||
|
||||
@@ -38,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Scott Frederick
|
||||
* @author Rafael Ceccone
|
||||
*/
|
||||
@ExtendWith(MavenBuildExtension.class)
|
||||
@DisabledIfDockerUnavailable
|
||||
@@ -292,6 +293,19 @@ class BuildImageTests extends AbstractArchiveIntegrationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void whenBuildImageIsInvokedWithTags(MavenBuild mavenBuild) {
|
||||
mavenBuild.project("build-image-tags").goals("package")
|
||||
.systemProperty("spring-boot.build-image.pullPolicy", "IF_NOT_PRESENT").execute((project) -> {
|
||||
assertThat(buildLog(project)).contains("Building image")
|
||||
.contains("docker.io/library/build-image-tags:0.0.1.BUILD-SNAPSHOT")
|
||||
.contains("Successfully built image").contains("docker.io/library/build-image-tags:latest")
|
||||
.contains("Successfully created image tag");
|
||||
removeImage("build-image-tags", "0.0.1.BUILD-SNAPSHOT");
|
||||
removeImage("build-image-tags", "latest");
|
||||
});
|
||||
}
|
||||
|
||||
@TestTemplate
|
||||
void failsWhenBuildImageIsInvokedOnMultiModuleProjectWithBuildImageGoal(MavenBuild mavenBuild) {
|
||||
mavenBuild.project("build-image-multi-module").goals("spring-boot:build-image")
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.springframework.boot.maven.it</groupId>
|
||||
<artifactId>build-image-tags</artifactId>
|
||||
<version>0.0.1.BUILD-SNAPSHOT</version>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>@java.version@</maven.compiler.source>
|
||||
<maven.compiler.target>@java.version@</maven.compiler.target>
|
||||
</properties>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>@project.groupId@</groupId>
|
||||
<artifactId>@project.artifactId@</artifactId>
|
||||
<version>@project.version@</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build-image</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<image>
|
||||
<builder>projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1</builder>
|
||||
<tags>
|
||||
<tag>${project.artifactId}:latest</tag>
|
||||
</tags>
|
||||
</image>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2012-2021 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.test;
|
||||
|
||||
public class SampleApplication {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("Launched");
|
||||
synchronized(args) {
|
||||
args.wait(); // Prevent exit
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Jeroen Meijer
|
||||
* @author Rafael Ceccone
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class Image {
|
||||
@@ -66,6 +67,8 @@ public class Image {
|
||||
|
||||
String network;
|
||||
|
||||
List<String> tags;
|
||||
|
||||
/**
|
||||
* The name of the created image.
|
||||
* @return the image name
|
||||
@@ -206,6 +209,9 @@ public class Image {
|
||||
request = request.withBindings(this.bindings.stream().map(Binding::of).collect(Collectors.toList()));
|
||||
}
|
||||
request = request.withNetwork(this.network);
|
||||
if (!CollectionUtils.isEmpty(this.tags)) {
|
||||
request = request.withTags(this.tags.stream().map(ImageReference::of).collect(Collectors.toList()));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.boot.buildpack.platform.build.BuildRequest;
|
||||
import org.springframework.boot.buildpack.platform.build.BuildpackReference;
|
||||
import org.springframework.boot.buildpack.platform.build.PullPolicy;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.Binding;
|
||||
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||
import org.springframework.boot.buildpack.platform.io.Owner;
|
||||
import org.springframework.boot.buildpack.platform.io.TarArchive;
|
||||
|
||||
@@ -42,6 +43,7 @@ import static org.assertj.core.api.Assertions.entry;
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @author Jeroen Meijer
|
||||
* @author Rafael Ceccone
|
||||
*/
|
||||
class ImageTests {
|
||||
|
||||
@@ -156,6 +158,15 @@ class ImageTests {
|
||||
assertThat(request.getNetwork()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBuildRequestWhenHasTagsUsesTags() {
|
||||
Image image = new Image();
|
||||
image.tags = Arrays.asList("my-app:latest", "example.com/my-app:0.0.1-SNAPSHOT", "example.com/my-app:latest");
|
||||
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
|
||||
assertThat(request.getTags()).containsExactly(ImageReference.of("my-app:latest"),
|
||||
ImageReference.of("example.com/my-app:0.0.1-SNAPSHOT"), ImageReference.of("example.com/my-app:latest"));
|
||||
}
|
||||
|
||||
private Artifact createArtifact() {
|
||||
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
|
||||
"jar", null, new DefaultArtifactHandler());
|
||||
|
||||
Reference in New Issue
Block a user