From 66f44b0c7f8e8851d9b67cad352ffc7c10107075 Mon Sep 17 00:00:00 2001 From: Rafael Ceccone Date: Mon, 9 Aug 2021 16:47:43 +0200 Subject: [PATCH] Add option to create tags for a built image This commit adds configuration to the Maven and Gradle plugins to allow specifying multiple tag to be created that refer to the built image. See gh-27613 --- .../platform/build/AbstractBuildLog.java | 7 +++ .../buildpack/platform/build/BuildLog.java | 7 +++ .../platform/build/BuildRequest.java | 61 +++++++++++++++---- .../buildpack/platform/build/Builder.java | 16 +++++ .../buildpack/platform/docker/DockerApi.java | 8 +++ .../platform/build/BuildRequestTests.java | 20 ++++++ .../platform/build/BuilderTests.java | 60 ++++++++++++++++++ .../build/PrintStreamBuildLogTests.java | 6 +- .../platform/docker/DockerApiTests.java | 25 ++++++++ .../platform/build/print-stream-build-log.txt | 2 + .../docs/asciidoc/packaging-oci-image.adoc | 5 ++ .../gradle/tasks/bundling/BootBuildImage.java | 47 ++++++++++++++ .../BootBuildImageIntegrationTests.java | 29 ++++++++- .../tasks/bundling/BootBuildImageTests.java | 32 ++++++++++ ...IntegrationTests-buildsImageWithTag.gradle | 12 ++++ ...rationTests-failsWithInvalidTagName.gradle | 12 ++++ .../docs/asciidoc/packaging-oci-image.adoc | 4 ++ .../boot/maven/BuildImageTests.java | 14 +++++ .../intTest/projects/build-image-tags/pom.xml | 37 +++++++++++ .../main/java/org/test/SampleApplication.java | 28 +++++++++ .../org/springframework/boot/maven/Image.java | 6 ++ .../boot/maven/ImageTests.java | 11 ++++ 22 files changed, 433 insertions(+), 16 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithTag.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithInvalidTagName.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-tags/pom.xml create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-tags/src/main/java/org/test/SampleApplication.java 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 7fc7ee08c2..213745c254 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 @@ -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 createdTag(ImageReference tag) { + log("Successfully created image tag '" + tag + "'"); + log(); + } + private String getDigest(Image image) { List digests = image.getDigests(); return (digests.isEmpty() ? "" : digests.get(0)); 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 6a88ea471f..23958ac09b 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 @@ -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 createdTag(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 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 b7feaa5277..0d637a79eb 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 @@ -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 tags; + BuildRequest(ImageReference name, Function 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 applicationContent, ImageReference builder, ImageReference runImage, Creator creator, Map env, boolean cleanCache, boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks, - List bindings, String network) { + List bindings, String network, List 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 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 getTags() { + return this.tags; + } + /** * 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 755d49e5e0..9f47c6f30c 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 @@ -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,10 @@ public class Builder { this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none()); try { executeLifecycle(request, ephemeralBuilder); + createTags(request.getName(), request.getTags()); if (request.isPublish()) { pushImage(request.getName()); + pushTags(request.getTags()); } } finally { @@ -157,6 +160,19 @@ public class Builder { this.log.pushedImage(reference); } + private void createTags(ImageReference sourceReference, List tags) throws IOException { + for (ImageReference tag : tags) { + this.docker.image().tag(sourceReference, tag); + this.log.createdTag(tag); + } + } + + private void pushTags(List tags) throws IOException { + for (ImageReference tag : tags) { + pushImage(tag); + } + } + private String getBuilderAuthHeader() { return (this.dockerConfiguration != null && this.dockerConfiguration.getBuilderRegistryAuthentication() != null) ? this.dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader() : null; 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 c07523d544..24d83d70c3 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 @@ -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 { @@ -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); + } + } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index 1af03c5e28..d3008b96a5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -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) null)) + .withMessage("Tags must not be null"); + } + private void hasExpectedJarContent(TarArchive archive) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 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 8e17071958..38bef618b6 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 @@ -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 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 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(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java index ec0ec4f3f7..a20e94b88e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PrintStreamBuildLogTests.java @@ -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 pullBuildImageConsumer = log.pullingImage(builderImageReference, ImageType.BUILDER); @@ -73,6 +76,7 @@ class PrintStreamBuildLogTests { phase2Consumer.accept(mockLogEvent("spring")); phase2Consumer.accept(mockLogEvent("boot")); log.executedLifecycle(request); + log.createdTag(tag); String expected = FileCopyUtils.copyToString(new InputStreamReader( getClass().getResourceAsStream("print-stream-build-log.txt"), StandardCharsets.UTF_8)); assertThat(out.toString()).isEqualToIgnoringNewLines(expected); 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 912d5af975..3270e041bb 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 @@ -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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/print-stream-build-log.txt b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/print-stream-build-log.txt index 83cfdffd0a..6fcfc7ee2c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/print-stream-build-log.txt +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/print-stream-build-log.txt @@ -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' diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index 42768558e3..b15fd7e157 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -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` +| +| Multiple {spring-boot-api}/buildpack/platform/docker/type/ImageReference.html#of-java.lang.String-[tag names] to be created for the generated image. +| + |=== NOTE: The plugin detects the target Java compatibility of the project using the JavaPlugin's `targetCompatibility` property. 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 23f9f57953..b986dd6ff1 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 @@ -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 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 getTags() { + return this.tags.getOrNull(); + } + + /** + * Sets the tags that will be created for the built image. + * @param tags the tags + */ + public void setTags(List 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 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 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() + ".*"; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index 8f5d4691af..ee85a516e1 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -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); } 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 6b54503892..fee0fea21f 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 @@ -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")); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithTag.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithTag.gradle new file mode 100644 index 0000000000..198699e7fe --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-buildsImageWithTag.gradle @@ -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" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithInvalidTagName.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithInvalidTagName.gradle new file mode 100644 index 0000000000..3c350dd1fb --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests-failsWithInvalidTagName.gradle @@ -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" ] +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index df8aa32b50..46ae88ebdd 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -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` +| Multiple {spring-boot-api}/buildpack/platform/docker/type/ImageReference.html#of-java.lang.String-[tag names] to be created for 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java index bae4b5f081..ce3b9a103d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/BuildImageTests.java @@ -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") diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-tags/pom.xml b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-tags/pom.xml new file mode 100644 index 0000000000..61e478cdd3 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-tags/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + org.springframework.boot.maven.it + build-image-tags + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + build-image + + + + projects.registry.vmware.com/springboot/spring-boot-cnb-builder:0.0.1 + + ${project.artifactId}:latest + + + + + + + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-tags/src/main/java/org/test/SampleApplication.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-tags/src/main/java/org/test/SampleApplication.java new file mode 100644 index 0000000000..e964724dea --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/intTest/projects/build-image-tags/src/main/java/org/test/SampleApplication.java @@ -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 + } + } + +} 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 e648f556bc..b2f7692d40 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 @@ -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 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; } 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 7c0d24921f..bef9d7d111 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 @@ -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());