Skip to content

Commit 09b627d

Browse files
Add support for publishing docker images to a registry
This commit adds options to the Maven and Gradle plugins to publish to a Docker registry the image generated by the image-building goal and task. The Docker registry auth configuration added in an earlier commit was modified to accept separate auth configs for the builder/run image and the generated image, since it is likely these images will be stored in separate registries or repositories with distinct auth required for each. Fixes gh-21001
1 parent 8b740c0 commit 09b627d

File tree

61 files changed

+1646
-442
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1646
-442
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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ public void pulledImage(Image image, ImageType imageType) {
7575
log(String.format(" > Pulled %s '%s'", imageType.getDescription(), getDigest(image)));
7676
}
7777

78+
@Override
79+
public Consumer<TotalProgressEvent> pushingImage(ImageReference imageReference) {
80+
return getProgressConsumer(String.format(" > Pushing image '%s'", imageReference));
81+
}
82+
83+
@Override
84+
public void pushedImage(ImageReference imageReference) {
85+
log(String.format(" > Pushed image '%s'", imageReference));
86+
}
87+
7888
@Override
7989
public void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume) {
8090
log(" > Executing lifecycle version " + version);

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,24 @@ public interface BuildLog {
9292

9393
/**
9494
* Log that an image has been pulled.
95-
* @param image the builder image that was pulled
95+
* @param image the image that was pulled
9696
* @param imageType the image type that was pulled
9797
*/
9898
void pulledImage(Image image, ImageType imageType);
9999

100+
/**
101+
* Log that an image is being pushed.
102+
* @param imageReference the image reference
103+
* @return a consumer for progress update events
104+
*/
105+
Consumer<TotalProgressEvent> pushingImage(ImageReference imageReference);
106+
107+
/**
108+
* Log that an image has been pushed.
109+
* @param imageReference the image reference
110+
*/
111+
void pushedImage(ImageReference imageReference);
112+
100113
/**
101114
* Log that the lifecycle is executing.
102115
* @param request the build request

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

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public class BuildRequest {
5959

6060
private final PullPolicy pullPolicy;
6161

62+
private final boolean publish;
63+
6264
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
6365
Assert.notNull(name, "Name must not be null");
6466
Assert.notNull(applicationContent, "ApplicationContent must not be null");
@@ -70,12 +72,13 @@ public class BuildRequest {
7072
this.cleanCache = false;
7173
this.verboseLogging = false;
7274
this.pullPolicy = PullPolicy.ALWAYS;
75+
this.publish = false;
7376
this.creator = Creator.withVersion("");
7477
}
7578

7679
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
7780
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
78-
boolean verboseLogging, PullPolicy pullPolicy) {
81+
boolean verboseLogging, PullPolicy pullPolicy, boolean publish) {
7982
this.name = name;
8083
this.applicationContent = applicationContent;
8184
this.builder = builder;
@@ -85,6 +88,7 @@ public class BuildRequest {
8588
this.cleanCache = cleanCache;
8689
this.verboseLogging = verboseLogging;
8790
this.pullPolicy = pullPolicy;
91+
this.publish = publish;
8892
}
8993

9094
/**
@@ -95,7 +99,7 @@ public class BuildRequest {
9599
public BuildRequest withBuilder(ImageReference builder) {
96100
Assert.notNull(builder, "Builder must not be null");
97101
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
98-
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy);
102+
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
99103
}
100104

101105
/**
@@ -105,7 +109,7 @@ public BuildRequest withBuilder(ImageReference builder) {
105109
*/
106110
public BuildRequest withRunImage(ImageReference runImageName) {
107111
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
108-
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy);
112+
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
109113
}
110114

111115
/**
@@ -116,7 +120,7 @@ public BuildRequest withRunImage(ImageReference runImageName) {
116120
public BuildRequest withCreator(Creator creator) {
117121
Assert.notNull(creator, "Creator must not be null");
118122
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
119-
this.cleanCache, this.verboseLogging, this.pullPolicy);
123+
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
120124
}
121125

122126
/**
@@ -131,7 +135,7 @@ public BuildRequest withEnv(String name, String value) {
131135
Map<String, String> env = new LinkedHashMap<>(this.env);
132136
env.put(name, value);
133137
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
134-
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy);
138+
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
135139
}
136140

137141
/**
@@ -144,7 +148,8 @@ public BuildRequest withEnv(Map<String, String> env) {
144148
Map<String, String> updatedEnv = new LinkedHashMap<>(this.env);
145149
updatedEnv.putAll(env);
146150
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
147-
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy);
151+
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy,
152+
this.publish);
148153
}
149154

150155
/**
@@ -154,7 +159,7 @@ public BuildRequest withEnv(Map<String, String> env) {
154159
*/
155160
public BuildRequest withCleanCache(boolean cleanCache) {
156161
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
157-
cleanCache, this.verboseLogging, this.pullPolicy);
162+
cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
158163
}
159164

160165
/**
@@ -164,7 +169,7 @@ public BuildRequest withCleanCache(boolean cleanCache) {
164169
*/
165170
public BuildRequest withVerboseLogging(boolean verboseLogging) {
166171
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
167-
this.cleanCache, verboseLogging, this.pullPolicy);
172+
this.cleanCache, verboseLogging, this.pullPolicy, this.publish);
168173
}
169174

170175
/**
@@ -174,7 +179,17 @@ public BuildRequest withVerboseLogging(boolean verboseLogging) {
174179
*/
175180
public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
176181
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
177-
this.cleanCache, this.verboseLogging, pullPolicy);
182+
this.cleanCache, this.verboseLogging, pullPolicy, this.publish);
183+
}
184+
185+
/**
186+
* Return a new {@link BuildRequest} with an updated publish setting.
187+
* @param publish if the built image should be pushed to a registry
188+
* @return an updated build request
189+
*/
190+
public BuildRequest withPublish(boolean publish) {
191+
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
192+
this.cleanCache, this.verboseLogging, this.pullPolicy, publish);
178193
}
179194

180195
/**
@@ -244,6 +259,14 @@ public boolean isVerboseLogging() {
244259
return this.verboseLogging;
245260
}
246261

262+
/**
263+
* Return if the built image should be pushed to a registry.
264+
* @return if the built image should be pushed to a registry
265+
*/
266+
public boolean isPublish() {
267+
return this.publish;
268+
}
269+
247270
/**
248271
* Return the image {@link PullPolicy} that the builder should use.
249272
* @return image pull policy

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.springframework.boot.buildpack.platform.docker.DockerApi;
2424
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
2525
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
26+
import org.springframework.boot.buildpack.platform.docker.TotalProgressPushListener;
2627
import org.springframework.boot.buildpack.platform.docker.UpdateListener;
2728
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
2829
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
@@ -45,6 +46,8 @@ public class Builder {
4546

4647
private final DockerApi docker;
4748

49+
private final DockerConfiguration dockerConfiguration;
50+
4851
/**
4952
* Create a new builder instance.
5053
*/
@@ -66,7 +69,7 @@ public Builder(DockerConfiguration dockerConfiguration) {
6669
* @param log a logger used to record output
6770
*/
6871
public Builder(BuildLog log) {
69-
this(log, new DockerApi());
72+
this(log, new DockerApi(), null);
7073
}
7174

7275
/**
@@ -76,13 +79,14 @@ public Builder(BuildLog log) {
7679
* @since 2.4.0
7780
*/
7881
public Builder(BuildLog log, DockerConfiguration dockerConfiguration) {
79-
this(log, new DockerApi(dockerConfiguration));
82+
this(log, new DockerApi(dockerConfiguration), dockerConfiguration);
8083
}
8184

82-
Builder(BuildLog log, DockerApi docker) {
85+
Builder(BuildLog log, DockerApi docker, DockerConfiguration dockerConfiguration) {
8386
Assert.notNull(log, "Log must not be null");
8487
this.log = log;
8588
this.docker = docker;
89+
this.dockerConfiguration = dockerConfiguration;
8690
}
8791

8892
public void build(BuildRequest request) throws DockerEngineException, IOException {
@@ -97,6 +101,9 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio
97101
this.docker.image().load(builder.getArchive(), UpdateListener.none());
98102
try {
99103
executeLifecycle(request, builder);
104+
if (request.isPublish()) {
105+
pushImage(request.getName());
106+
}
100107
}
101108
finally {
102109
this.docker.image().remove(builder.getName(), true);
@@ -143,11 +150,28 @@ private Image getImage(BuildRequest request, ImageType imageType) throws IOExcep
143150
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
144151
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingImage(reference, imageType);
145152
TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer);
146-
Image image = this.docker.image().pull(reference, listener);
153+
Image image = this.docker.image().pull(reference, listener, getBuilderAuthHeader());
147154
this.log.pulledImage(image, imageType);
148155
return image;
149156
}
150157

158+
private void pushImage(ImageReference reference) throws IOException {
159+
Consumer<TotalProgressEvent> progressConsumer = this.log.pushingImage(reference);
160+
TotalProgressPushListener listener = new TotalProgressPushListener(progressConsumer);
161+
this.docker.image().push(reference, listener, getPublishAuthHeader());
162+
this.log.pushedImage(reference);
163+
}
164+
165+
private String getBuilderAuthHeader() {
166+
return (this.dockerConfiguration != null && this.dockerConfiguration.getBuilderRegistryAuthentication() != null)
167+
? this.dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader() : null;
168+
}
169+
170+
private String getPublishAuthHeader() {
171+
return (this.dockerConfiguration != null && this.dockerConfiguration.getPublishRegistryAuthentication() != null)
172+
? this.dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader() : null;
173+
}
174+
151175
private void assertStackIdsMatch(Image runImage, Image builderImage) {
152176
StackId runImageStackId = StackId.fromImage(runImage);
153177
StackId builderImageStackId = StackId.fromImage(builderImage);

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

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public DockerApi() {
7878
* @since 2.4.0
7979
*/
8080
public DockerApi(DockerConfiguration dockerConfiguration) {
81-
this(HttpTransport.create(dockerConfiguration));
81+
this(HttpTransport.create((dockerConfiguration != null) ? dockerConfiguration.getHost() : null));
8282
}
8383

8484
/**
@@ -156,13 +156,26 @@ public class ImageApi {
156156
* @throws IOException on IO error
157157
*/
158158
public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent> listener) throws IOException {
159+
return pull(reference, listener, null);
160+
}
161+
162+
/**
163+
* Pull an image from a registry.
164+
* @param reference the image reference to pull
165+
* @param listener a pull listener to receive update events
166+
* @param registryAuth registry authentication credentials
167+
* @return the {@link ImageApi pulled image} instance
168+
* @throws IOException on IO error
169+
*/
170+
public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent> listener, String registryAuth)
171+
throws IOException {
159172
Assert.notNull(reference, "Reference must not be null");
160173
Assert.notNull(listener, "Listener must not be null");
161174
URI createUri = buildUrl("/images/create", "fromImage", reference.toString());
162175
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
163176
listener.onStart();
164177
try {
165-
try (Response response = http().post(createUri)) {
178+
try (Response response = http().post(createUri, registryAuth)) {
166179
jsonStream().get(response.getContent(), PullImageUpdateEvent.class, (event) -> {
167180
digestCapture.onUpdate(event);
168181
listener.onUpdate(event);
@@ -175,6 +188,33 @@ public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent>
175188
}
176189
}
177190

191+
/**
192+
* Push an image to a registry.
193+
* @param reference the image reference to push
194+
* @param listener a push listener to receive update events
195+
* @param registryAuth registry authentication credentials
196+
* @throws IOException on IO error
197+
*/
198+
public void push(ImageReference reference, UpdateListener<PushImageUpdateEvent> listener, String registryAuth)
199+
throws IOException {
200+
Assert.notNull(reference, "Reference must not be null");
201+
Assert.notNull(listener, "Listener must not be null");
202+
URI pushUri = buildUrl("/images/" + reference + "/push");
203+
ErrorCaptureUpdateListener errorListener = new ErrorCaptureUpdateListener();
204+
listener.onStart();
205+
try {
206+
try (Response response = http().post(pushUri, registryAuth)) {
207+
jsonStream().get(response.getContent(), PushImageUpdateEvent.class, (event) -> {
208+
errorListener.onUpdate(event);
209+
listener.onUpdate(event);
210+
});
211+
}
212+
}
213+
finally {
214+
listener.onFinish();
215+
}
216+
}
217+
178218
/**
179219
* Load an {@link ImageArchive} into Docker.
180220
* @param archive the archive to load
@@ -398,4 +438,18 @@ String getCapturedStream() {
398438

399439
}
400440

441+
/**
442+
* {@link UpdateListener} used to capture the details of an error in a response
443+
* stream.
444+
*/
445+
private static class ErrorCaptureUpdateListener implements UpdateListener<PushImageUpdateEvent> {
446+
447+
@Override
448+
public void onUpdate(PushImageUpdateEvent event) {
449+
Assert.state(event.getErrorDetail() == null,
450+
() -> "Error response received when pushing image: " + event.getErrorDetail().getMessage());
451+
}
452+
453+
}
454+
401455
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.buildpack.platform.docker;
18+
19+
/**
20+
* A {@link ProgressUpdateEvent} fired for image events.
21+
*
22+
* @author Phillip Webb
23+
* @author Scott Frederick
24+
* @since 2.4.0
25+
*/
26+
public class ImageProgressUpdateEvent extends ProgressUpdateEvent {
27+
28+
private final String id;
29+
30+
protected ImageProgressUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress) {
31+
super(status, progressDetail, progress);
32+
this.id = id;
33+
}
34+
35+
/**
36+
* Returns the ID of the image layer being updated if available.
37+
* @return the ID of the updated layer or {@code null}
38+
*/
39+
public String getId() {
40+
return this.id;
41+
}
42+
43+
}

0 commit comments

Comments
 (0)