Skip to content

Commit 66f44b0

Browse files
cecconescottfrederick
authored andcommitted
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
1 parent ca69c8b commit 66f44b0

File tree

22 files changed

+433
-16
lines changed

22 files changed

+433
-16
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/AbstractBuildLog.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* @author Phillip Webb
3232
* @author Scott Frederick
3333
* @author Andrey Shlykov
34+
* @author Rafael Ceccone
3435
* @since 2.3.0
3536
*/
3637
public abstract class AbstractBuildLog implements BuildLog {
@@ -89,6 +90,12 @@ public void executedLifecycle(BuildRequest request) {
8990
log();
9091
}
9192

93+
@Override
94+
public void createdTag(ImageReference tag) {
95+
log("Successfully created image tag '" + tag + "'");
96+
log();
97+
}
98+
9299
private String getDigest(Image image) {
93100
List<String> digests = image.getDigests();
94101
return (digests.isEmpty() ? "" : digests.get(0));

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildLog.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* @author Phillip Webb
3232
* @author Scott Frederick
3333
* @author Andrey Shlykov
34+
* @author Rafael Ceccone
3435
* @since 2.3.0
3536
* @see #toSystemOut()
3637
*/
@@ -99,6 +100,12 @@ public interface BuildLog {
99100
*/
100101
void executedLifecycle(BuildRequest request);
101102

103+
/**
104+
* Log that a tag has been created.
105+
* @param tag the tag reference
106+
*/
107+
void createdTag(ImageReference tag);
108+
102109
/**
103110
* Factory method that returns a {@link BuildLog} the outputs to {@link System#out}.
104111
* @return a build log instance that logs to system out

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
* @author Scott Frederick
3838
* @author Andrey Shlykov
3939
* @author Jeroen Meijer
40+
* @author Rafael Ceccone
4041
* @since 2.3.0
4142
*/
4243
public class BuildRequest {
@@ -71,6 +72,8 @@ public class BuildRequest {
7172

7273
private final String network;
7374

75+
private final List<ImageReference> tags;
76+
7477
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
7578
Assert.notNull(name, "Name must not be null");
7679
Assert.notNull(applicationContent, "ApplicationContent must not be null");
@@ -87,12 +90,13 @@ public class BuildRequest {
8790
this.buildpacks = Collections.emptyList();
8891
this.bindings = Collections.emptyList();
8992
this.network = null;
93+
this.tags = Collections.emptyList();
9094
}
9195

9296
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
9397
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
9498
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks,
95-
List<Binding> bindings, String network) {
99+
List<Binding> bindings, String network, List<ImageReference> tags) {
96100
this.name = name;
97101
this.applicationContent = applicationContent;
98102
this.builder = builder;
@@ -106,6 +110,7 @@ public class BuildRequest {
106110
this.buildpacks = buildpacks;
107111
this.bindings = bindings;
108112
this.network = network;
113+
this.tags = tags;
109114
}
110115

111116
/**
@@ -117,7 +122,7 @@ public BuildRequest withBuilder(ImageReference builder) {
117122
Assert.notNull(builder, "Builder must not be null");
118123
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
119124
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
120-
this.buildpacks, this.bindings, this.network);
125+
this.buildpacks, this.bindings, this.network, this.tags);
121126
}
122127

123128
/**
@@ -128,7 +133,7 @@ public BuildRequest withBuilder(ImageReference builder) {
128133
public BuildRequest withRunImage(ImageReference runImageName) {
129134
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
130135
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
131-
this.buildpacks, this.bindings, this.network);
136+
this.buildpacks, this.bindings, this.network, this.tags);
132137
}
133138

134139
/**
@@ -140,7 +145,7 @@ public BuildRequest withCreator(Creator creator) {
140145
Assert.notNull(creator, "Creator must not be null");
141146
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
142147
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
143-
this.network);
148+
this.network, this.tags);
144149
}
145150

146151
/**
@@ -156,7 +161,7 @@ public BuildRequest withEnv(String name, String value) {
156161
env.put(name, value);
157162
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
158163
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
159-
this.buildpacks, this.bindings, this.network);
164+
this.buildpacks, this.bindings, this.network, this.tags);
160165
}
161166

162167
/**
@@ -170,7 +175,7 @@ public BuildRequest withEnv(Map<String, String> env) {
170175
updatedEnv.putAll(env);
171176
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
172177
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy,
173-
this.publish, this.buildpacks, this.bindings, this.network);
178+
this.publish, this.buildpacks, this.bindings, this.network, this.tags);
174179
}
175180

176181
/**
@@ -181,7 +186,7 @@ public BuildRequest withEnv(Map<String, String> env) {
181186
public BuildRequest withCleanCache(boolean cleanCache) {
182187
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
183188
cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
184-
this.network);
189+
this.network, this.tags);
185190
}
186191

187192
/**
@@ -192,7 +197,7 @@ public BuildRequest withCleanCache(boolean cleanCache) {
192197
public BuildRequest withVerboseLogging(boolean verboseLogging) {
193198
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
194199
this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
195-
this.network);
200+
this.network, this.tags);
196201
}
197202

198203
/**
@@ -203,7 +208,7 @@ public BuildRequest withVerboseLogging(boolean verboseLogging) {
203208
public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
204209
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
205210
this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings,
206-
this.network);
211+
this.network, this.tags);
207212
}
208213

209214
/**
@@ -214,7 +219,7 @@ public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
214219
public BuildRequest withPublish(boolean publish) {
215220
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
216221
this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings,
217-
this.network);
222+
this.network, this.tags);
218223
}
219224

220225
/**
@@ -238,7 +243,7 @@ public BuildRequest withBuildpacks(List<BuildpackReference> buildpacks) {
238243
Assert.notNull(buildpacks, "Buildpacks must not be null");
239244
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
240245
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings,
241-
this.network);
246+
this.network, this.tags);
242247
}
243248

244249
/**
@@ -262,7 +267,7 @@ public BuildRequest withBindings(List<Binding> bindings) {
262267
Assert.notNull(bindings, "Bindings must not be null");
263268
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
264269
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings,
265-
this.network);
270+
this.network, this.tags);
266271
}
267272

268273
/**
@@ -274,7 +279,29 @@ public BuildRequest withBindings(List<Binding> bindings) {
274279
public BuildRequest withNetwork(String network) {
275280
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
276281
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
277-
network);
282+
network, this.tags);
283+
}
284+
285+
/**
286+
* Return a new {@link BuildRequest} with updated tags.
287+
* @param tags a collection of tags to be created for the built image
288+
* @return an updated build request
289+
*/
290+
public BuildRequest withTags(ImageReference... tags) {
291+
Assert.notEmpty(tags, "Tags must not be empty");
292+
return withTags(Arrays.asList(tags));
293+
}
294+
295+
/**
296+
* Return a new {@link BuildRequest} with updated tags.
297+
* @param tags a collection of tags to be created for the built image
298+
* @return an updated build request
299+
*/
300+
public BuildRequest withTags(List<ImageReference> tags) {
301+
Assert.notNull(tags, "Tags must not be null");
302+
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
303+
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
304+
this.network, tags);
278305
}
279306

280307
/**
@@ -386,6 +413,14 @@ public String getNetwork() {
386413
return this.network;
387414
}
388415

416+
/**
417+
* Return the collection of tags that should be created.
418+
* @return the tags
419+
*/
420+
public List<ImageReference> getTags() {
421+
return this.tags;
422+
}
423+
389424
/**
390425
* Factory method to create a new {@link BuildRequest} from a JAR file.
391426
* @param jarFile the source jar file

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
* @author Phillip Webb
4242
* @author Scott Frederick
4343
* @author Andrey Shlykov
44+
* @author Rafael Ceccone
4445
* @since 2.3.0
4546
*/
4647
public class Builder {
@@ -110,8 +111,10 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio
110111
this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none());
111112
try {
112113
executeLifecycle(request, ephemeralBuilder);
114+
createTags(request.getName(), request.getTags());
113115
if (request.isPublish()) {
114116
pushImage(request.getName());
117+
pushTags(request.getTags());
115118
}
116119
}
117120
finally {
@@ -157,6 +160,19 @@ private void pushImage(ImageReference reference) throws IOException {
157160
this.log.pushedImage(reference);
158161
}
159162

163+
private void createTags(ImageReference sourceReference, List<ImageReference> tags) throws IOException {
164+
for (ImageReference tag : tags) {
165+
this.docker.image().tag(sourceReference, tag);
166+
this.log.createdTag(tag);
167+
}
168+
}
169+
170+
private void pushTags(List<ImageReference> tags) throws IOException {
171+
for (ImageReference tag : tags) {
172+
pushImage(tag);
173+
}
174+
}
175+
160176
private String getBuilderAuthHeader() {
161177
return (this.dockerConfiguration != null && this.dockerConfiguration.getBuilderRegistryAuthentication() != null)
162178
? this.dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader() : null;

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
*
5353
* @author Phillip Webb
5454
* @author Scott Frederick
55+
* @author Rafael Ceccone
5556
* @since 2.3.0
5657
*/
5758
public class DockerApi {
@@ -300,6 +301,13 @@ public Image inspect(ImageReference reference) throws IOException {
300301
}
301302
}
302303

304+
public void tag(ImageReference sourceReference, ImageReference targetReference) throws IOException {
305+
Assert.notNull(sourceReference, "SourceReference must not be null");
306+
Assert.notNull(targetReference, "TargetReference must not be null");
307+
URI uri = buildUrl("/images/" + sourceReference + "/tag", "repo", targetReference.toString());
308+
http().post(uri);
309+
}
310+
303311
}
304312

305313
/**

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
* @author Phillip Webb
4747
* @author Scott Frederick
4848
* @author Jeroen Meijer
49+
* @author Rafael Ceccone
4950
*/
5051
class BuildRequestTests {
5152

@@ -206,6 +207,25 @@ void withNetworkUpdatesNetwork() throws IOException {
206207
assertThat(request.getNetwork()).isEqualTo("test");
207208
}
208209

210+
@Test
211+
void withTagsAddsTags() throws IOException {
212+
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
213+
BuildRequest witTags = request.withTags(ImageReference.of("docker.io/library/my-app:latest"),
214+
ImageReference.of("example.com/custom/my-app:0.0.1"),
215+
ImageReference.of("example.com/custom/my-app:latest"));
216+
assertThat(request.getTags()).isEmpty();
217+
assertThat(witTags.getTags()).containsExactly(ImageReference.of("docker.io/library/my-app:latest"),
218+
ImageReference.of("example.com/custom/my-app:0.0.1"),
219+
ImageReference.of("example.com/custom/my-app:latest"));
220+
}
221+
222+
@Test
223+
void withTagsWhenTagsIsNullThrowsException() throws IOException {
224+
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
225+
assertThatIllegalArgumentException().isThrownBy(() -> request.withTags((List<ImageReference>) null))
226+
.withMessage("Tags must not be null");
227+
}
228+
209229
private void hasExpectedJarContent(TarArchive archive) {
210230
try {
211231
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
*
5959
* @author Phillip Webb
6060
* @author Scott Frederick
61+
* @author Rafael Ceccone
6162
*/
6263
class BuilderTests {
6364

@@ -276,6 +277,65 @@ void buildInvokesBuilderWithIfNotPresentPullPolicy() throws Exception {
276277
verify(docker.image(), times(2)).pull(any(), any(), isNull());
277278
}
278279

280+
@Test
281+
void buildInvokesBuilderWithTags() throws Exception {
282+
TestPrintStream out = new TestPrintStream();
283+
DockerApi docker = mockDockerApi();
284+
Image builderImage = loadImage("image.json");
285+
Image runImage = loadImage("run-image.json");
286+
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
287+
.willAnswer(withPulledImage(builderImage));
288+
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
289+
.willAnswer(withPulledImage(runImage));
290+
Builder builder = new Builder(BuildLog.to(out), docker, null);
291+
BuildRequest request = getTestRequest().withTags(ImageReference.of("my-application:1.2.3"));
292+
builder.build(request);
293+
assertThat(out.toString()).contains("Running creator");
294+
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
295+
assertThat(out.toString()).contains("Successfully created image tag 'docker.io/library/my-application:1.2.3'");
296+
verify(docker.image()).tag(eq(request.getName()), eq(ImageReference.of("my-application:1.2.3")));
297+
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
298+
verify(docker.image()).load(archive.capture(), any());
299+
verify(docker.image()).remove(archive.getValue().getTag(), true);
300+
}
301+
302+
@Test
303+
void buildInvokesBuilderWithTagsAndPublishesImageAndTags() throws Exception {
304+
TestPrintStream out = new TestPrintStream();
305+
DockerApi docker = mockDockerApi();
306+
Image builderImage = loadImage("image.json");
307+
Image runImage = loadImage("run-image.json");
308+
DockerConfiguration dockerConfiguration = new DockerConfiguration()
309+
.withBuilderRegistryTokenAuthentication("builder token")
310+
.withPublishRegistryTokenAuthentication("publish token");
311+
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(),
312+
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
313+
.willAnswer(withPulledImage(builderImage));
314+
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
315+
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
316+
.willAnswer(withPulledImage(runImage));
317+
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
318+
BuildRequest request = getTestRequest().withPublish(true).withTags(ImageReference.of("my-application:1.2.3"));
319+
builder.build(request);
320+
assertThat(out.toString()).contains("Running creator");
321+
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
322+
assertThat(out.toString()).contains("Successfully created image tag 'docker.io/library/my-application:1.2.3'");
323+
324+
verify(docker.image()).pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(),
325+
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
326+
verify(docker.image()).pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
327+
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
328+
verify(docker.image()).push(eq(request.getName()), any(),
329+
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
330+
verify(docker.image()).tag(eq(request.getName()), eq(ImageReference.of("my-application:1.2.3")));
331+
verify(docker.image()).push(eq(ImageReference.of("my-application:1.2.3")), any(),
332+
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
333+
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
334+
verify(docker.image()).load(archive.capture(), any());
335+
verify(docker.image()).remove(archive.getValue().getTag(), true);
336+
verifyNoMoreInteractions(docker.image());
337+
}
338+
279339
@Test
280340
void buildWhenStackIdDoesNotMatchThrowsException() throws Exception {
281341
TestPrintStream out = new TestPrintStream();

0 commit comments

Comments
 (0)