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
This commit is contained in:
parent
8b740c07e3
commit
09b627d232
|
|
@ -75,6 +75,16 @@ public abstract class AbstractBuildLog implements BuildLog {
|
||||||
log(String.format(" > Pulled %s '%s'", imageType.getDescription(), getDigest(image)));
|
log(String.format(" > Pulled %s '%s'", imageType.getDescription(), getDigest(image)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Consumer<TotalProgressEvent> pushingImage(ImageReference imageReference) {
|
||||||
|
return getProgressConsumer(String.format(" > Pushing image '%s'", imageReference));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pushedImage(ImageReference imageReference) {
|
||||||
|
log(String.format(" > Pushed image '%s'", imageReference));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume) {
|
public void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume) {
|
||||||
log(" > Executing lifecycle version " + version);
|
log(" > Executing lifecycle version " + version);
|
||||||
|
|
|
||||||
|
|
@ -92,11 +92,24 @@ public interface BuildLog {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log that an image has been pulled.
|
* Log that an image has been pulled.
|
||||||
* @param image the builder image that was pulled
|
* @param image the image that was pulled
|
||||||
* @param imageType the image type that was pulled
|
* @param imageType the image type that was pulled
|
||||||
*/
|
*/
|
||||||
void pulledImage(Image image, ImageType imageType);
|
void pulledImage(Image image, ImageType imageType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log that an image is being pushed.
|
||||||
|
* @param imageReference the image reference
|
||||||
|
* @return a consumer for progress update events
|
||||||
|
*/
|
||||||
|
Consumer<TotalProgressEvent> pushingImage(ImageReference imageReference);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log that an image has been pushed.
|
||||||
|
* @param imageReference the image reference
|
||||||
|
*/
|
||||||
|
void pushedImage(ImageReference imageReference);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log that the lifecycle is executing.
|
* Log that the lifecycle is executing.
|
||||||
* @param request the build request
|
* @param request the build request
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ public class BuildRequest {
|
||||||
|
|
||||||
private final PullPolicy pullPolicy;
|
private final PullPolicy pullPolicy;
|
||||||
|
|
||||||
|
private final boolean publish;
|
||||||
|
|
||||||
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
|
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
|
||||||
Assert.notNull(name, "Name must not be null");
|
Assert.notNull(name, "Name must not be null");
|
||||||
Assert.notNull(applicationContent, "ApplicationContent must not be null");
|
Assert.notNull(applicationContent, "ApplicationContent must not be null");
|
||||||
|
|
@ -70,12 +72,13 @@ public class BuildRequest {
|
||||||
this.cleanCache = false;
|
this.cleanCache = false;
|
||||||
this.verboseLogging = false;
|
this.verboseLogging = false;
|
||||||
this.pullPolicy = PullPolicy.ALWAYS;
|
this.pullPolicy = PullPolicy.ALWAYS;
|
||||||
|
this.publish = false;
|
||||||
this.creator = Creator.withVersion("");
|
this.creator = Creator.withVersion("");
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
|
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
|
||||||
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
|
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
|
||||||
boolean verboseLogging, PullPolicy pullPolicy) {
|
boolean verboseLogging, PullPolicy pullPolicy, boolean publish) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.applicationContent = applicationContent;
|
this.applicationContent = applicationContent;
|
||||||
this.builder = builder;
|
this.builder = builder;
|
||||||
|
|
@ -85,6 +88,7 @@ public class BuildRequest {
|
||||||
this.cleanCache = cleanCache;
|
this.cleanCache = cleanCache;
|
||||||
this.verboseLogging = verboseLogging;
|
this.verboseLogging = verboseLogging;
|
||||||
this.pullPolicy = pullPolicy;
|
this.pullPolicy = pullPolicy;
|
||||||
|
this.publish = publish;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -95,7 +99,7 @@ public class BuildRequest {
|
||||||
public BuildRequest withBuilder(ImageReference builder) {
|
public BuildRequest withBuilder(ImageReference builder) {
|
||||||
Assert.notNull(builder, "Builder must not be null");
|
Assert.notNull(builder, "Builder must not be null");
|
||||||
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
|
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
|
||||||
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy);
|
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -105,7 +109,7 @@ public class BuildRequest {
|
||||||
*/
|
*/
|
||||||
public BuildRequest withRunImage(ImageReference runImageName) {
|
public BuildRequest withRunImage(ImageReference runImageName) {
|
||||||
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
|
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
|
||||||
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy);
|
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -116,7 +120,7 @@ public class BuildRequest {
|
||||||
public BuildRequest withCreator(Creator creator) {
|
public BuildRequest withCreator(Creator creator) {
|
||||||
Assert.notNull(creator, "Creator must not be null");
|
Assert.notNull(creator, "Creator must not be null");
|
||||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
|
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
|
||||||
this.cleanCache, this.verboseLogging, this.pullPolicy);
|
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -131,7 +135,7 @@ public class BuildRequest {
|
||||||
Map<String, String> env = new LinkedHashMap<>(this.env);
|
Map<String, String> env = new LinkedHashMap<>(this.env);
|
||||||
env.put(name, value);
|
env.put(name, value);
|
||||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
|
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
|
||||||
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy);
|
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -144,7 +148,8 @@ public class BuildRequest {
|
||||||
Map<String, String> updatedEnv = new LinkedHashMap<>(this.env);
|
Map<String, String> updatedEnv = new LinkedHashMap<>(this.env);
|
||||||
updatedEnv.putAll(env);
|
updatedEnv.putAll(env);
|
||||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
|
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
|
||||||
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy);
|
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy,
|
||||||
|
this.publish);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -154,7 +159,7 @@ public class BuildRequest {
|
||||||
*/
|
*/
|
||||||
public BuildRequest withCleanCache(boolean cleanCache) {
|
public BuildRequest withCleanCache(boolean cleanCache) {
|
||||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||||
cleanCache, this.verboseLogging, this.pullPolicy);
|
cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -164,7 +169,7 @@ public class BuildRequest {
|
||||||
*/
|
*/
|
||||||
public BuildRequest withVerboseLogging(boolean verboseLogging) {
|
public BuildRequest withVerboseLogging(boolean verboseLogging) {
|
||||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||||
this.cleanCache, verboseLogging, this.pullPolicy);
|
this.cleanCache, verboseLogging, this.pullPolicy, this.publish);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -174,7 +179,17 @@ public class BuildRequest {
|
||||||
*/
|
*/
|
||||||
public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
|
public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
|
||||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||||
this.cleanCache, this.verboseLogging, pullPolicy);
|
this.cleanCache, this.verboseLogging, pullPolicy, this.publish);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a new {@link BuildRequest} with an updated publish setting.
|
||||||
|
* @param publish if the built image should be pushed to a registry
|
||||||
|
* @return an updated build request
|
||||||
|
*/
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -244,6 +259,14 @@ public class BuildRequest {
|
||||||
return this.verboseLogging;
|
return this.verboseLogging;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return if the built image should be pushed to a registry.
|
||||||
|
* @return if the built image should be pushed to a registry
|
||||||
|
*/
|
||||||
|
public boolean isPublish() {
|
||||||
|
return this.publish;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the image {@link PullPolicy} that the builder should use.
|
* Return the image {@link PullPolicy} that the builder should use.
|
||||||
* @return image pull policy
|
* @return image pull policy
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import org.springframework.boot.buildpack.platform.build.BuilderMetadata.Stack;
|
||||||
import org.springframework.boot.buildpack.platform.docker.DockerApi;
|
import org.springframework.boot.buildpack.platform.docker.DockerApi;
|
||||||
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
|
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
|
||||||
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
|
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.TotalProgressPushListener;
|
||||||
import org.springframework.boot.buildpack.platform.docker.UpdateListener;
|
import org.springframework.boot.buildpack.platform.docker.UpdateListener;
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
||||||
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
|
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
|
||||||
|
|
@ -45,6 +46,8 @@ public class Builder {
|
||||||
|
|
||||||
private final DockerApi docker;
|
private final DockerApi docker;
|
||||||
|
|
||||||
|
private final DockerConfiguration dockerConfiguration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new builder instance.
|
* Create a new builder instance.
|
||||||
*/
|
*/
|
||||||
|
|
@ -66,7 +69,7 @@ public class Builder {
|
||||||
* @param log a logger used to record output
|
* @param log a logger used to record output
|
||||||
*/
|
*/
|
||||||
public Builder(BuildLog log) {
|
public Builder(BuildLog log) {
|
||||||
this(log, new DockerApi());
|
this(log, new DockerApi(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -76,13 +79,14 @@ public class Builder {
|
||||||
* @since 2.4.0
|
* @since 2.4.0
|
||||||
*/
|
*/
|
||||||
public Builder(BuildLog log, DockerConfiguration dockerConfiguration) {
|
public Builder(BuildLog log, DockerConfiguration dockerConfiguration) {
|
||||||
this(log, new DockerApi(dockerConfiguration));
|
this(log, new DockerApi(dockerConfiguration), dockerConfiguration);
|
||||||
}
|
}
|
||||||
|
|
||||||
Builder(BuildLog log, DockerApi docker) {
|
Builder(BuildLog log, DockerApi docker, DockerConfiguration dockerConfiguration) {
|
||||||
Assert.notNull(log, "Log must not be null");
|
Assert.notNull(log, "Log must not be null");
|
||||||
this.log = log;
|
this.log = log;
|
||||||
this.docker = docker;
|
this.docker = docker;
|
||||||
|
this.dockerConfiguration = dockerConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void build(BuildRequest request) throws DockerEngineException, IOException {
|
public void build(BuildRequest request) throws DockerEngineException, IOException {
|
||||||
|
|
@ -97,6 +101,9 @@ public class Builder {
|
||||||
this.docker.image().load(builder.getArchive(), UpdateListener.none());
|
this.docker.image().load(builder.getArchive(), UpdateListener.none());
|
||||||
try {
|
try {
|
||||||
executeLifecycle(request, builder);
|
executeLifecycle(request, builder);
|
||||||
|
if (request.isPublish()) {
|
||||||
|
pushImage(request.getName());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
this.docker.image().remove(builder.getName(), true);
|
this.docker.image().remove(builder.getName(), true);
|
||||||
|
|
@ -143,11 +150,28 @@ public class Builder {
|
||||||
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
|
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
|
||||||
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingImage(reference, imageType);
|
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingImage(reference, imageType);
|
||||||
TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer);
|
TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer);
|
||||||
Image image = this.docker.image().pull(reference, listener);
|
Image image = this.docker.image().pull(reference, listener, getBuilderAuthHeader());
|
||||||
this.log.pulledImage(image, imageType);
|
this.log.pulledImage(image, imageType);
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void pushImage(ImageReference reference) throws IOException {
|
||||||
|
Consumer<TotalProgressEvent> progressConsumer = this.log.pushingImage(reference);
|
||||||
|
TotalProgressPushListener listener = new TotalProgressPushListener(progressConsumer);
|
||||||
|
this.docker.image().push(reference, listener, getPublishAuthHeader());
|
||||||
|
this.log.pushedImage(reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getBuilderAuthHeader() {
|
||||||
|
return (this.dockerConfiguration != null && this.dockerConfiguration.getBuilderRegistryAuthentication() != null)
|
||||||
|
? this.dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getPublishAuthHeader() {
|
||||||
|
return (this.dockerConfiguration != null && this.dockerConfiguration.getPublishRegistryAuthentication() != null)
|
||||||
|
? this.dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader() : null;
|
||||||
|
}
|
||||||
|
|
||||||
private void assertStackIdsMatch(Image runImage, Image builderImage) {
|
private void assertStackIdsMatch(Image runImage, Image builderImage) {
|
||||||
StackId runImageStackId = StackId.fromImage(runImage);
|
StackId runImageStackId = StackId.fromImage(runImage);
|
||||||
StackId builderImageStackId = StackId.fromImage(builderImage);
|
StackId builderImageStackId = StackId.fromImage(builderImage);
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ public class DockerApi {
|
||||||
* @since 2.4.0
|
* @since 2.4.0
|
||||||
*/
|
*/
|
||||||
public DockerApi(DockerConfiguration dockerConfiguration) {
|
public DockerApi(DockerConfiguration dockerConfiguration) {
|
||||||
this(HttpTransport.create(dockerConfiguration));
|
this(HttpTransport.create((dockerConfiguration != null) ? dockerConfiguration.getHost() : null));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -156,13 +156,26 @@ public class DockerApi {
|
||||||
* @throws IOException on IO error
|
* @throws IOException on IO error
|
||||||
*/
|
*/
|
||||||
public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent> listener) throws IOException {
|
public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent> listener) throws IOException {
|
||||||
|
return pull(reference, listener, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull an image from a registry.
|
||||||
|
* @param reference the image reference to pull
|
||||||
|
* @param listener a pull listener to receive update events
|
||||||
|
* @param registryAuth registry authentication credentials
|
||||||
|
* @return the {@link ImageApi pulled image} instance
|
||||||
|
* @throws IOException on IO error
|
||||||
|
*/
|
||||||
|
public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent> listener, String registryAuth)
|
||||||
|
throws IOException {
|
||||||
Assert.notNull(reference, "Reference must not be null");
|
Assert.notNull(reference, "Reference must not be null");
|
||||||
Assert.notNull(listener, "Listener must not be null");
|
Assert.notNull(listener, "Listener must not be null");
|
||||||
URI createUri = buildUrl("/images/create", "fromImage", reference.toString());
|
URI createUri = buildUrl("/images/create", "fromImage", reference.toString());
|
||||||
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
|
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
|
||||||
listener.onStart();
|
listener.onStart();
|
||||||
try {
|
try {
|
||||||
try (Response response = http().post(createUri)) {
|
try (Response response = http().post(createUri, registryAuth)) {
|
||||||
jsonStream().get(response.getContent(), PullImageUpdateEvent.class, (event) -> {
|
jsonStream().get(response.getContent(), PullImageUpdateEvent.class, (event) -> {
|
||||||
digestCapture.onUpdate(event);
|
digestCapture.onUpdate(event);
|
||||||
listener.onUpdate(event);
|
listener.onUpdate(event);
|
||||||
|
|
@ -175,6 +188,33 @@ public class DockerApi {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push an image to a registry.
|
||||||
|
* @param reference the image reference to push
|
||||||
|
* @param listener a push listener to receive update events
|
||||||
|
* @param registryAuth registry authentication credentials
|
||||||
|
* @throws IOException on IO error
|
||||||
|
*/
|
||||||
|
public void push(ImageReference reference, UpdateListener<PushImageUpdateEvent> listener, String registryAuth)
|
||||||
|
throws IOException {
|
||||||
|
Assert.notNull(reference, "Reference must not be null");
|
||||||
|
Assert.notNull(listener, "Listener must not be null");
|
||||||
|
URI pushUri = buildUrl("/images/" + reference + "/push");
|
||||||
|
ErrorCaptureUpdateListener errorListener = new ErrorCaptureUpdateListener();
|
||||||
|
listener.onStart();
|
||||||
|
try {
|
||||||
|
try (Response response = http().post(pushUri, registryAuth)) {
|
||||||
|
jsonStream().get(response.getContent(), PushImageUpdateEvent.class, (event) -> {
|
||||||
|
errorListener.onUpdate(event);
|
||||||
|
listener.onUpdate(event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
listener.onFinish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load an {@link ImageArchive} into Docker.
|
* Load an {@link ImageArchive} into Docker.
|
||||||
* @param archive the archive to load
|
* @param archive the archive to load
|
||||||
|
|
@ -398,4 +438,18 @@ public class DockerApi {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link UpdateListener} used to capture the details of an error in a response
|
||||||
|
* stream.
|
||||||
|
*/
|
||||||
|
private static class ErrorCaptureUpdateListener implements UpdateListener<PushImageUpdateEvent> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpdate(PushImageUpdateEvent event) {
|
||||||
|
Assert.state(event.getErrorDetail() == null,
|
||||||
|
() -> "Error response received when pushing image: " + event.getErrorDetail().getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2020 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.buildpack.platform.docker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ProgressUpdateEvent} fired for image events.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Scott Frederick
|
||||||
|
* @since 2.4.0
|
||||||
|
*/
|
||||||
|
public class ImageProgressUpdateEvent extends ProgressUpdateEvent {
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
protected ImageProgressUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress) {
|
||||||
|
super(status, progressDetail, progress);
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of the image layer being updated if available.
|
||||||
|
* @return the ID of the updated layer or {@code null}
|
||||||
|
*/
|
||||||
|
public String getId() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -22,24 +22,14 @@ import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
* A {@link ProgressUpdateEvent} fired as an image is pulled.
|
* A {@link ProgressUpdateEvent} fired as an image is pulled.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Scott Frederick
|
||||||
* @since 2.3.0
|
* @since 2.3.0
|
||||||
*/
|
*/
|
||||||
public class PullImageUpdateEvent extends ProgressUpdateEvent {
|
public class PullImageUpdateEvent extends ImageProgressUpdateEvent {
|
||||||
|
|
||||||
private final String id;
|
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public PullImageUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress) {
|
public PullImageUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress) {
|
||||||
super(status, progressDetail, progress);
|
super(id, status, progressDetail, progress);
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the ID of the layer being updated if available.
|
|
||||||
* @return the ID of the updated layer or {@code null}
|
|
||||||
*/
|
|
||||||
public String getId() {
|
|
||||||
return this.id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2020 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.buildpack.platform.docker;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link ProgressUpdateEvent} fired as an image is pushed to a registry.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
|
* @since 2.4.0
|
||||||
|
*/
|
||||||
|
public class PushImageUpdateEvent extends ImageProgressUpdateEvent {
|
||||||
|
|
||||||
|
private final ErrorDetail errorDetail;
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
public PushImageUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress,
|
||||||
|
ErrorDetail errorDetail) {
|
||||||
|
super(id, status, progressDetail, progress);
|
||||||
|
this.errorDetail = errorDetail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the details of any error encountered during processing.
|
||||||
|
* @return the error
|
||||||
|
*/
|
||||||
|
public ErrorDetail getErrorDetail() {
|
||||||
|
return this.errorDetail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details of an error embedded in a response stream.
|
||||||
|
*/
|
||||||
|
public static class ErrorDetail {
|
||||||
|
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
public ErrorDetail(@JsonProperty("message") String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the message field from the error detail.
|
||||||
|
* @return the message
|
||||||
|
*/
|
||||||
|
public String getMessage() {
|
||||||
|
return this.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2020 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.buildpack.platform.docker;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.ProgressUpdateEvent.ProgressDetail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link UpdateListener} that calculates the total progress of the entire image operation
|
||||||
|
* and publishes {@link TotalProgressEvent}.
|
||||||
|
*
|
||||||
|
* @param <E> the type of {@link ImageProgressUpdateEvent}
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Scott Frederick
|
||||||
|
* @since 2.4.0
|
||||||
|
*/
|
||||||
|
public abstract class TotalProgressListener<E extends ImageProgressUpdateEvent> implements UpdateListener<E> {
|
||||||
|
|
||||||
|
private final Map<String, Layer> layers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final Consumer<TotalProgressEvent> consumer;
|
||||||
|
|
||||||
|
private final String[] trackedStatusKeys;
|
||||||
|
|
||||||
|
private boolean progressStarted;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link TotalProgressListener} that sends {@link TotalProgressEvent
|
||||||
|
* events} to the given consumer.
|
||||||
|
* @param consumer the consumer that receives {@link TotalProgressEvent progress
|
||||||
|
* events}
|
||||||
|
* @param trackedStatusKeys a list of status event keys to track the progress of
|
||||||
|
*/
|
||||||
|
protected TotalProgressListener(Consumer<TotalProgressEvent> consumer, String[] trackedStatusKeys) {
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.trackedStatusKeys = trackedStatusKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpdate(E event) {
|
||||||
|
if (event.getId() != null) {
|
||||||
|
this.layers.computeIfAbsent(event.getId(), (value) -> new Layer(this.trackedStatusKeys)).update(event);
|
||||||
|
}
|
||||||
|
this.progressStarted = this.progressStarted || event.getProgress() != null;
|
||||||
|
if (this.progressStarted) {
|
||||||
|
publish(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFinish() {
|
||||||
|
this.layers.values().forEach(Layer::finish);
|
||||||
|
publish(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publish(int fallback) {
|
||||||
|
int count = 0;
|
||||||
|
int total = 0;
|
||||||
|
for (Layer layer : this.layers.values()) {
|
||||||
|
count++;
|
||||||
|
total += layer.getProgress();
|
||||||
|
}
|
||||||
|
TotalProgressEvent event = new TotalProgressEvent(
|
||||||
|
(count != 0) ? withinPercentageBounds(total / count) : fallback);
|
||||||
|
this.consumer.accept(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int withinPercentageBounds(int value) {
|
||||||
|
if (value < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Math.min(value, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Progress for an individual layer.
|
||||||
|
*/
|
||||||
|
private static class Layer {
|
||||||
|
|
||||||
|
private final Map<String, Integer> progressByStatus = new HashMap<>();
|
||||||
|
|
||||||
|
Layer(String[] trackedStatusKeys) {
|
||||||
|
Arrays.stream(trackedStatusKeys).forEach((status) -> this.progressByStatus.put(status, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(ImageProgressUpdateEvent event) {
|
||||||
|
String status = event.getStatus();
|
||||||
|
if (event.getProgressDetail() != null && this.progressByStatus.containsKey(status)) {
|
||||||
|
int current = this.progressByStatus.get(status);
|
||||||
|
this.progressByStatus.put(status, updateProgress(current, event.getProgressDetail()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int updateProgress(int current, ProgressDetail detail) {
|
||||||
|
int result = withinPercentageBounds((int) ((100.0 / detail.getTotal()) * detail.getCurrent()));
|
||||||
|
return Math.max(result, current);
|
||||||
|
}
|
||||||
|
|
||||||
|
void finish() {
|
||||||
|
this.progressByStatus.keySet().forEach((key) -> this.progressByStatus.put(key, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
int getProgress() {
|
||||||
|
return withinPercentageBounds((this.progressByStatus.values().stream().mapToInt(Integer::valueOf).sum())
|
||||||
|
/ this.progressByStatus.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -16,26 +16,19 @@
|
||||||
|
|
||||||
package org.springframework.boot.buildpack.platform.docker;
|
package org.springframework.boot.buildpack.platform.docker;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.ProgressUpdateEvent.ProgressDetail;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link UpdateListener} that calculates the total progress of the entire pull operation
|
* {@link UpdateListener} that calculates the total progress of the entire pull operation
|
||||||
* and publishes {@link TotalProgressEvent}.
|
* and publishes {@link TotalProgressEvent}.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Scott Frederick
|
||||||
* @since 2.3.0
|
* @since 2.3.0
|
||||||
*/
|
*/
|
||||||
public class TotalProgressPullListener implements UpdateListener<PullImageUpdateEvent> {
|
public class TotalProgressPullListener extends TotalProgressListener<PullImageUpdateEvent> {
|
||||||
|
|
||||||
private final Map<String, Layer> layers = new ConcurrentHashMap<>();
|
private static final String[] TRACKED_STATUS_KEYS = { "Downloading", "Extracting" };
|
||||||
|
|
||||||
private final Consumer<TotalProgressEvent> consumer;
|
|
||||||
|
|
||||||
private boolean progressStarted;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link TotalProgressPullListener} that prints a progress bar to
|
* Create a new {@link TotalProgressPullListener} that prints a progress bar to
|
||||||
|
|
@ -53,87 +46,7 @@ public class TotalProgressPullListener implements UpdateListener<PullImageUpdate
|
||||||
* events}
|
* events}
|
||||||
*/
|
*/
|
||||||
public TotalProgressPullListener(Consumer<TotalProgressEvent> consumer) {
|
public TotalProgressPullListener(Consumer<TotalProgressEvent> consumer) {
|
||||||
this.consumer = consumer;
|
super(consumer, TRACKED_STATUS_KEYS);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onUpdate(PullImageUpdateEvent event) {
|
|
||||||
if (event.getId() != null) {
|
|
||||||
this.layers.computeIfAbsent(event.getId(), Layer::new).update(event);
|
|
||||||
}
|
|
||||||
this.progressStarted = this.progressStarted || event.getProgress() != null;
|
|
||||||
if (this.progressStarted) {
|
|
||||||
publish(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFinish() {
|
|
||||||
this.layers.values().forEach(Layer::finish);
|
|
||||||
publish(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void publish(int fallback) {
|
|
||||||
int count = 0;
|
|
||||||
int total = 0;
|
|
||||||
for (Layer layer : this.layers.values()) {
|
|
||||||
count++;
|
|
||||||
total += layer.getProgress();
|
|
||||||
}
|
|
||||||
TotalProgressEvent event = new TotalProgressEvent(
|
|
||||||
(count != 0) ? withinPercentageBounds(total / count) : fallback);
|
|
||||||
this.consumer.accept(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int withinPercentageBounds(int value) {
|
|
||||||
if (value < 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return Math.min(value, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Progress for an individual layer.
|
|
||||||
*/
|
|
||||||
private static class Layer {
|
|
||||||
|
|
||||||
private int downloadProgress;
|
|
||||||
|
|
||||||
private int extractProgress;
|
|
||||||
|
|
||||||
Layer(String id) {
|
|
||||||
}
|
|
||||||
|
|
||||||
void update(PullImageUpdateEvent event) {
|
|
||||||
if (event.getProgressDetail() != null) {
|
|
||||||
ProgressDetail detail = event.getProgressDetail();
|
|
||||||
if ("Downloading".equals(event.getStatus())) {
|
|
||||||
this.downloadProgress = updateProgress(this.downloadProgress, detail);
|
|
||||||
}
|
|
||||||
if ("Extracting".equals(event.getStatus())) {
|
|
||||||
this.extractProgress = updateProgress(this.extractProgress, detail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int updateProgress(int current, ProgressDetail detail) {
|
|
||||||
int result = withinPercentageBounds((int) ((100.0 / detail.getTotal()) * detail.getCurrent()));
|
|
||||||
return Math.max(result, current);
|
|
||||||
}
|
|
||||||
|
|
||||||
void finish() {
|
|
||||||
this.downloadProgress = 100;
|
|
||||||
this.extractProgress = 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
int getProgress() {
|
|
||||||
return withinPercentageBounds((this.downloadProgress + this.extractProgress) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2020 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.buildpack.platform.docker;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link UpdateListener} that calculates the total progress of the entire push operation
|
||||||
|
* and publishes {@link TotalProgressEvent}.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
|
* @since 2.4.0
|
||||||
|
*/
|
||||||
|
public class TotalProgressPushListener extends TotalProgressListener<PushImageUpdateEvent> {
|
||||||
|
|
||||||
|
private static final String[] TRACKED_STATUS_KEYS = { "Pushing" };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link TotalProgressPushListener} that prints a progress bar to
|
||||||
|
* {@link System#out}.
|
||||||
|
* @param prefix the prefix to output
|
||||||
|
*/
|
||||||
|
public TotalProgressPushListener(String prefix) {
|
||||||
|
this(new TotalProgressBar(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link TotalProgressPushListener} that sends {@link TotalProgressEvent
|
||||||
|
* events} to the given consumer.
|
||||||
|
* @param consumer the consumer that receives {@link TotalProgressEvent progress
|
||||||
|
* events}
|
||||||
|
*/
|
||||||
|
public TotalProgressPushListener(Consumer<TotalProgressEvent> consumer) {
|
||||||
|
super(consumer, TRACKED_STATUS_KEYS);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -29,40 +29,65 @@ public final class DockerConfiguration {
|
||||||
|
|
||||||
private final DockerHost host;
|
private final DockerHost host;
|
||||||
|
|
||||||
private final DockerRegistryAuthentication authentication;
|
private final DockerRegistryAuthentication builderAuthentication;
|
||||||
|
|
||||||
|
private final DockerRegistryAuthentication publishAuthentication;
|
||||||
|
|
||||||
public DockerConfiguration() {
|
public DockerConfiguration() {
|
||||||
this(null, null);
|
this(null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private DockerConfiguration(DockerHost host, DockerRegistryAuthentication authentication) {
|
private DockerConfiguration(DockerHost host, DockerRegistryAuthentication builderAuthentication,
|
||||||
|
DockerRegistryAuthentication publishAuthentication) {
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.authentication = authentication;
|
this.builderAuthentication = builderAuthentication;
|
||||||
|
this.publishAuthentication = publishAuthentication;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DockerHost getHost() {
|
public DockerHost getHost() {
|
||||||
return this.host;
|
return this.host;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DockerRegistryAuthentication getRegistryAuthentication() {
|
public DockerRegistryAuthentication getBuilderRegistryAuthentication() {
|
||||||
return this.authentication;
|
return this.builderAuthentication;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DockerRegistryAuthentication getPublishRegistryAuthentication() {
|
||||||
|
return this.publishAuthentication;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DockerConfiguration withHost(String address, boolean secure, String certificatePath) {
|
public DockerConfiguration withHost(String address, boolean secure, String certificatePath) {
|
||||||
Assert.notNull(address, "Address must not be null");
|
Assert.notNull(address, "Address must not be null");
|
||||||
return new DockerConfiguration(new DockerHost(address, secure, certificatePath), this.authentication);
|
return new DockerConfiguration(new DockerHost(address, secure, certificatePath), this.builderAuthentication,
|
||||||
|
this.publishAuthentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DockerConfiguration withRegistryTokenAuthentication(String token) {
|
public DockerConfiguration withBuilderRegistryTokenAuthentication(String token) {
|
||||||
Assert.notNull(token, "Token must not be null");
|
Assert.notNull(token, "Token must not be null");
|
||||||
return new DockerConfiguration(this.host, new DockerRegistryTokenAuthentication(token));
|
return new DockerConfiguration(this.host, new DockerRegistryTokenAuthentication(token),
|
||||||
|
this.publishAuthentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DockerConfiguration withRegistryUserAuthentication(String username, String password, String url,
|
public DockerConfiguration withBuilderRegistryUserAuthentication(String username, String password, String url,
|
||||||
String email) {
|
String email) {
|
||||||
Assert.notNull(username, "Username must not be null");
|
Assert.notNull(username, "Username must not be null");
|
||||||
Assert.notNull(password, "Password must not be null");
|
Assert.notNull(password, "Password must not be null");
|
||||||
return new DockerConfiguration(this.host, new DockerRegistryUserAuthentication(username, password, url, email));
|
return new DockerConfiguration(this.host, new DockerRegistryUserAuthentication(username, password, url, email),
|
||||||
|
this.publishAuthentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DockerConfiguration withPublishRegistryTokenAuthentication(String token) {
|
||||||
|
Assert.notNull(token, "Token must not be null");
|
||||||
|
return new DockerConfiguration(this.host, this.builderAuthentication,
|
||||||
|
new DockerRegistryTokenAuthentication(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public DockerConfiguration withPublishRegistryUserAuthentication(String username, String password, String url,
|
||||||
|
String email) {
|
||||||
|
Assert.notNull(username, "Username must not be null");
|
||||||
|
Assert.notNull(password, "Password must not be null");
|
||||||
|
return new DockerConfiguration(this.host, this.builderAuthentication,
|
||||||
|
new DockerRegistryUserAuthentication(username, password, url, email));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ public class DockerHost {
|
||||||
|
|
||||||
private final String certificatePath;
|
private final String certificatePath;
|
||||||
|
|
||||||
protected DockerHost(String address, boolean secure, String certificatePath) {
|
public DockerHost(String address, boolean secure, String certificatePath) {
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.secure = secure;
|
this.secure = secure;
|
||||||
this.certificatePath = certificatePath;
|
this.certificatePath = certificatePath;
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@ package org.springframework.boot.buildpack.platform.docker.configuration;
|
||||||
public interface DockerRegistryAuthentication {
|
public interface DockerRegistryAuthentication {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the auth header that should be used for docker authentication.
|
* Returns the auth header that should be used for docker authentication.
|
||||||
* @return the auth header
|
* @return the auth header
|
||||||
*/
|
*/
|
||||||
String createAuthHeader();
|
String getAuthHeader();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ class DockerRegistryTokenAuthentication extends JsonEncodedDockerRegistryAuthent
|
||||||
|
|
||||||
DockerRegistryTokenAuthentication(String token) {
|
DockerRegistryTokenAuthentication(String token) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
createAuthHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
String getToken() {
|
String getToken() {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ class DockerRegistryUserAuthentication extends JsonEncodedDockerRegistryAuthenti
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
|
createAuthHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
String getUsername() {
|
String getUsername() {
|
||||||
|
|
|
||||||
|
|
@ -22,17 +22,23 @@ import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
||||||
import org.springframework.util.Base64Utils;
|
import org.springframework.util.Base64Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link DockerRegistryAuthentication} that uses creates a Base64 encoded auth header
|
* {@link DockerRegistryAuthentication} that uses a Base64 encoded auth header value based
|
||||||
* value based on the JSON created from the instance.
|
* on the JSON created from the instance.
|
||||||
*
|
*
|
||||||
* @author Scott Frederick
|
* @author Scott Frederick
|
||||||
*/
|
*/
|
||||||
class JsonEncodedDockerRegistryAuthentication implements DockerRegistryAuthentication {
|
class JsonEncodedDockerRegistryAuthentication implements DockerRegistryAuthentication {
|
||||||
|
|
||||||
|
private String authHeader;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String createAuthHeader() {
|
public String getAuthHeader() {
|
||||||
|
return this.authHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void createAuthHeader() {
|
||||||
try {
|
try {
|
||||||
return Base64Utils.encodeToUrlSafeString(SharedObjectMapper.get().writeValueAsBytes(this));
|
this.authHeader = Base64Utils.encodeToUrlSafeString(SharedObjectMapper.get().writeValueAsBytes(this));
|
||||||
}
|
}
|
||||||
catch (JsonProcessingException ex) {
|
catch (JsonProcessingException ex) {
|
||||||
throw new IllegalStateException("Error creating Docker registry authentication header", ex);
|
throw new IllegalStateException("Error creating Docker registry authentication header", ex);
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ import org.apache.http.client.methods.HttpUriRequest;
|
||||||
import org.apache.http.entity.AbstractHttpEntity;
|
import org.apache.http.entity.AbstractHttpEntity;
|
||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
|
|
||||||
import org.springframework.boot.buildpack.platform.io.Content;
|
import org.springframework.boot.buildpack.platform.io.Content;
|
||||||
import org.springframework.boot.buildpack.platform.io.IOConsumer;
|
import org.springframework.boot.buildpack.platform.io.IOConsumer;
|
||||||
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
||||||
|
|
@ -53,19 +52,17 @@ import org.springframework.util.StringUtils;
|
||||||
*/
|
*/
|
||||||
abstract class HttpClientTransport implements HttpTransport {
|
abstract class HttpClientTransport implements HttpTransport {
|
||||||
|
|
||||||
|
static final String REGISTRY_AUTH_HEADER = "X-Registry-Auth";
|
||||||
|
|
||||||
private final CloseableHttpClient client;
|
private final CloseableHttpClient client;
|
||||||
|
|
||||||
private final HttpHost host;
|
private final HttpHost host;
|
||||||
|
|
||||||
private final String registryAuthHeader;
|
protected HttpClientTransport(CloseableHttpClient client, HttpHost host) {
|
||||||
|
|
||||||
protected HttpClientTransport(CloseableHttpClient client, HttpHost host,
|
|
||||||
DockerRegistryAuthentication authentication) {
|
|
||||||
Assert.notNull(client, "Client must not be null");
|
Assert.notNull(client, "Client must not be null");
|
||||||
Assert.notNull(host, "Host must not be null");
|
Assert.notNull(host, "Host must not be null");
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.host = host;
|
this.host = host;
|
||||||
this.registryAuthHeader = buildRegistryAuthHeader(authentication);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -88,6 +85,17 @@ abstract class HttpClientTransport implements HttpTransport {
|
||||||
return execute(new HttpPost(uri));
|
return execute(new HttpPost(uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a HTTP POST operation.
|
||||||
|
* @param uri the destination URI
|
||||||
|
* @param registryAuth registry authentication credentials
|
||||||
|
* @return the operation response
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Response post(URI uri, String registryAuth) {
|
||||||
|
return execute(new HttpPost(uri), registryAuth);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a HTTP POST operation.
|
* Perform a HTTP POST operation.
|
||||||
* @param uri the destination URI
|
* @param uri the destination URI
|
||||||
|
|
@ -122,11 +130,6 @@ abstract class HttpClientTransport implements HttpTransport {
|
||||||
return execute(new HttpDelete(uri));
|
return execute(new HttpDelete(uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildRegistryAuthHeader(DockerRegistryAuthentication authentication) {
|
|
||||||
String authHeader = (authentication != null) ? authentication.createAuthHeader() : null;
|
|
||||||
return (StringUtils.hasText(authHeader)) ? authHeader : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Response execute(HttpEntityEnclosingRequestBase request, String contentType,
|
private Response execute(HttpEntityEnclosingRequestBase request, String contentType,
|
||||||
IOConsumer<OutputStream> writer) {
|
IOConsumer<OutputStream> writer) {
|
||||||
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
|
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
|
||||||
|
|
@ -134,11 +137,15 @@ abstract class HttpClientTransport implements HttpTransport {
|
||||||
return execute(request);
|
return execute(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Response execute(HttpEntityEnclosingRequestBase request, String registryAuth) {
|
||||||
|
if (StringUtils.hasText(registryAuth)) {
|
||||||
|
request.setHeader(REGISTRY_AUTH_HEADER, registryAuth);
|
||||||
|
}
|
||||||
|
return execute(request);
|
||||||
|
}
|
||||||
|
|
||||||
private Response execute(HttpUriRequest request) {
|
private Response execute(HttpUriRequest request) {
|
||||||
try {
|
try {
|
||||||
if (this.registryAuthHeader != null) {
|
|
||||||
request.addHeader("X-Registry-Auth", this.registryAuthHeader);
|
|
||||||
}
|
|
||||||
CloseableHttpResponse response = this.client.execute(this.host, request);
|
CloseableHttpResponse response = this.client.execute(this.host, request);
|
||||||
StatusLine statusLine = response.getStatusLine();
|
StatusLine statusLine = response.getStatusLine();
|
||||||
int statusCode = statusLine.getStatusCode();
|
int statusCode = statusLine.getStatusCode();
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import java.io.OutputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost;
|
||||||
import org.springframework.boot.buildpack.platform.io.IOConsumer;
|
import org.springframework.boot.buildpack.platform.io.IOConsumer;
|
||||||
import org.springframework.boot.buildpack.platform.system.Environment;
|
import org.springframework.boot.buildpack.platform.system.Environment;
|
||||||
|
|
||||||
|
|
@ -51,6 +52,15 @@ public interface HttpTransport {
|
||||||
*/
|
*/
|
||||||
Response post(URI uri) throws IOException;
|
Response post(URI uri) throws IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a HTTP POST operation.
|
||||||
|
* @param uri the destination URI (excluding any host/port)
|
||||||
|
* @param registryAuth registry authentication credentials
|
||||||
|
* @return the operation response
|
||||||
|
* @throws IOException on IO error
|
||||||
|
*/
|
||||||
|
Response post(URI uri, String registryAuth) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a HTTP POST operation.
|
* Perform a HTTP POST operation.
|
||||||
* @param uri the destination URI (excluding any host/port)
|
* @param uri the destination URI (excluding any host/port)
|
||||||
|
|
@ -85,17 +95,17 @@ public interface HttpTransport {
|
||||||
* @return a {@link HttpTransport} instance
|
* @return a {@link HttpTransport} instance
|
||||||
*/
|
*/
|
||||||
static HttpTransport create() {
|
static HttpTransport create() {
|
||||||
return create(new DockerConfiguration());
|
return create(Environment.SYSTEM);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the most suitable {@link HttpTransport} based on the
|
* Create the most suitable {@link HttpTransport} based on the
|
||||||
* {@link Environment#SYSTEM system environment}.
|
* {@link Environment#SYSTEM system environment}.
|
||||||
* @param dockerConfiguration the Docker engine configuration
|
* @param dockerHost the Docker engine host configuration
|
||||||
* @return a {@link HttpTransport} instance
|
* @return a {@link HttpTransport} instance
|
||||||
*/
|
*/
|
||||||
static HttpTransport create(DockerConfiguration dockerConfiguration) {
|
static HttpTransport create(DockerHost dockerHost) {
|
||||||
return create(Environment.SYSTEM, dockerConfiguration);
|
return create(Environment.SYSTEM, dockerHost);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -105,19 +115,19 @@ public interface HttpTransport {
|
||||||
* @return a {@link HttpTransport} instance
|
* @return a {@link HttpTransport} instance
|
||||||
*/
|
*/
|
||||||
static HttpTransport create(Environment environment) {
|
static HttpTransport create(Environment environment) {
|
||||||
return create(environment, new DockerConfiguration());
|
return create(environment, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the most suitable {@link HttpTransport} based on the given
|
* Create the most suitable {@link HttpTransport} based on the given
|
||||||
* {@link Environment} and {@link DockerConfiguration}.
|
* {@link Environment} and {@link DockerConfiguration}.
|
||||||
* @param environment the source environment
|
* @param environment the source environment
|
||||||
* @param dockerConfiguration the Docker engine configuration
|
* @param dockerHost the Docker engine host configuration
|
||||||
* @return a {@link HttpTransport} instance
|
* @return a {@link HttpTransport} instance
|
||||||
*/
|
*/
|
||||||
static HttpTransport create(Environment environment, DockerConfiguration dockerConfiguration) {
|
static HttpTransport create(Environment environment, DockerHost dockerHost) {
|
||||||
HttpTransport remote = RemoteHttpClientTransport.createIfPossible(environment, dockerConfiguration);
|
HttpTransport remote = RemoteHttpClientTransport.createIfPossible(environment, dockerHost);
|
||||||
return (remote != null) ? remote : LocalHttpClientTransport.create(environment, dockerConfiguration);
|
return (remote != null) ? remote : LocalHttpClientTransport.create(environment);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,6 @@ import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
|
||||||
import org.apache.http.protocol.HttpContext;
|
import org.apache.http.protocol.HttpContext;
|
||||||
import org.apache.http.util.Args;
|
import org.apache.http.util.Args;
|
||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
|
|
||||||
import org.springframework.boot.buildpack.platform.socket.DomainSocket;
|
import org.springframework.boot.buildpack.platform.socket.DomainSocket;
|
||||||
import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket;
|
import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket;
|
||||||
import org.springframework.boot.buildpack.platform.system.Environment;
|
import org.springframework.boot.buildpack.platform.system.Environment;
|
||||||
|
|
@ -58,16 +56,15 @@ final class LocalHttpClientTransport extends HttpClientTransport {
|
||||||
|
|
||||||
private static final HttpHost LOCAL_DOCKER_HOST = HttpHost.create("docker://localhost");
|
private static final HttpHost LOCAL_DOCKER_HOST = HttpHost.create("docker://localhost");
|
||||||
|
|
||||||
private LocalHttpClientTransport(CloseableHttpClient client, DockerRegistryAuthentication authentication) {
|
private LocalHttpClientTransport(CloseableHttpClient client) {
|
||||||
super(client, LOCAL_DOCKER_HOST, authentication);
|
super(client, LOCAL_DOCKER_HOST);
|
||||||
}
|
}
|
||||||
|
|
||||||
static LocalHttpClientTransport create(Environment environment, DockerConfiguration dockerConfiguration) {
|
static LocalHttpClientTransport create(Environment environment) {
|
||||||
HttpClientBuilder builder = HttpClients.custom();
|
HttpClientBuilder builder = HttpClients.custom();
|
||||||
builder.setConnectionManager(new LocalConnectionManager(socketFilePath(environment)));
|
builder.setConnectionManager(new LocalConnectionManager(socketFilePath(environment)));
|
||||||
builder.setSchemePortResolver(new LocalSchemePortResolver());
|
builder.setSchemePortResolver(new LocalSchemePortResolver());
|
||||||
return new LocalHttpClientTransport(builder.build(),
|
return new LocalHttpClientTransport(builder.build());
|
||||||
(dockerConfiguration != null) ? dockerConfiguration.getRegistryAuthentication() : null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String socketFilePath(Environment environment) {
|
private static String socketFilePath(Environment environment) {
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,7 @@ import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
import org.apache.http.impl.client.HttpClients;
|
import org.apache.http.impl.client.HttpClients;
|
||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost;
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost;
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory;
|
import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory;
|
||||||
import org.springframework.boot.buildpack.platform.system.Environment;
|
import org.springframework.boot.buildpack.platform.system.Environment;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
@ -51,23 +49,21 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
|
||||||
|
|
||||||
private static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH";
|
private static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH";
|
||||||
|
|
||||||
private RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host,
|
private RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host) {
|
||||||
DockerRegistryAuthentication authentication) {
|
super(client, host);
|
||||||
super(client, host, authentication);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static RemoteHttpClientTransport createIfPossible(Environment environment,
|
static RemoteHttpClientTransport createIfPossible(Environment environment, DockerHost dockerHost) {
|
||||||
DockerConfiguration dockerConfiguration) {
|
return createIfPossible(environment, dockerHost, new SslContextFactory());
|
||||||
return createIfPossible(environment, dockerConfiguration, new SslContextFactory());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static RemoteHttpClientTransport createIfPossible(Environment environment, DockerConfiguration dockerConfiguration,
|
static RemoteHttpClientTransport createIfPossible(Environment environment, DockerHost dockerHost,
|
||||||
SslContextFactory sslContextFactory) {
|
SslContextFactory sslContextFactory) {
|
||||||
DockerHost host = getHost(environment, dockerConfiguration);
|
DockerHost host = getHost(environment, dockerHost);
|
||||||
if (host == null || host.getAddress() == null || isLocalFileReference(host.getAddress())) {
|
if (host == null || host.getAddress() == null || isLocalFileReference(host.getAddress())) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return create(host, dockerConfiguration, sslContextFactory, HttpHost.create(host.getAddress()));
|
return create(host, sslContextFactory, HttpHost.create(host.getAddress()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isLocalFileReference(String host) {
|
private static boolean isLocalFileReference(String host) {
|
||||||
|
|
@ -80,16 +76,15 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RemoteHttpClientTransport create(DockerHost host, DockerConfiguration dockerConfiguration,
|
private static RemoteHttpClientTransport create(DockerHost host, SslContextFactory sslContextFactory,
|
||||||
SslContextFactory sslContextFactory, HttpHost tcpHost) {
|
HttpHost tcpHost) {
|
||||||
HttpClientBuilder builder = HttpClients.custom();
|
HttpClientBuilder builder = HttpClients.custom();
|
||||||
if (host.isSecure()) {
|
if (host.isSecure()) {
|
||||||
builder.setSSLSocketFactory(getSecureConnectionSocketFactory(host, sslContextFactory));
|
builder.setSSLSocketFactory(getSecureConnectionSocketFactory(host, sslContextFactory));
|
||||||
}
|
}
|
||||||
String scheme = host.isSecure() ? "https" : "http";
|
String scheme = host.isSecure() ? "https" : "http";
|
||||||
HttpHost httpHost = new HttpHost(tcpHost.getHostName(), tcpHost.getPort(), scheme);
|
HttpHost httpHost = new HttpHost(tcpHost.getHostName(), tcpHost.getPort(), scheme);
|
||||||
return new RemoteHttpClientTransport(builder.build(), httpHost,
|
return new RemoteHttpClientTransport(builder.build(), httpHost);
|
||||||
(dockerConfiguration != null) ? dockerConfiguration.getRegistryAuthentication() : null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LayeredConnectionSocketFactory getSecureConnectionSocketFactory(DockerHost host,
|
private static LayeredConnectionSocketFactory getSecureConnectionSocketFactory(DockerHost host,
|
||||||
|
|
@ -101,14 +96,11 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
|
||||||
return new SSLConnectionSocketFactory(sslContext);
|
return new SSLConnectionSocketFactory(sslContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DockerHost getHost(Environment environment, DockerConfiguration dockerConfiguration) {
|
private static DockerHost getHost(Environment environment, DockerHost dockerHost) {
|
||||||
if (environment.get(DOCKER_HOST) != null) {
|
if (environment.get(DOCKER_HOST) != null) {
|
||||||
return new EnvironmentDockerHost(environment);
|
return new EnvironmentDockerHost(environment);
|
||||||
}
|
}
|
||||||
if (dockerConfiguration != null && dockerConfiguration.getHost() != null) {
|
return dockerHost;
|
||||||
return dockerConfiguration.getHost();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class EnvironmentDockerHost extends DockerHost {
|
private static class EnvironmentDockerHost extends DockerHost {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi
|
||||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
|
import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi;
|
||||||
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
|
import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi;
|
||||||
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
|
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
||||||
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
|
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
|
||||||
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
|
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
|
||||||
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
|
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
|
||||||
|
|
@ -44,11 +45,13 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
import static org.mockito.BDDMockito.given;
|
import static org.mockito.BDDMockito.given;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link Builder}.
|
* Tests for {@link Builder}.
|
||||||
|
|
@ -83,18 +86,53 @@ class BuilderTests {
|
||||||
DockerApi docker = mockDockerApi();
|
DockerApi docker = mockDockerApi();
|
||||||
Image builderImage = loadImage("image.json");
|
Image builderImage = loadImage("image.json");
|
||||||
Image runImage = loadImage("run-image.json");
|
Image runImage = loadImage("run-image.json");
|
||||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(builderImage));
|
.willAnswer(withPulledImage(builderImage));
|
||||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any()))
|
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(runImage));
|
.willAnswer(withPulledImage(runImage));
|
||||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||||
BuildRequest request = getTestRequest();
|
BuildRequest request = getTestRequest();
|
||||||
builder.build(request);
|
builder.build(request);
|
||||||
assertThat(out.toString()).contains("Running creator");
|
assertThat(out.toString()).contains("Running creator");
|
||||||
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
|
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
|
||||||
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
|
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
|
||||||
|
verify(docker.image()).pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull());
|
||||||
|
verify(docker.image()).pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull());
|
||||||
verify(docker.image()).load(archive.capture(), any());
|
verify(docker.image()).load(archive.capture(), any());
|
||||||
verify(docker.image()).remove(archive.getValue().getTag(), true);
|
verify(docker.image()).remove(archive.getValue().getTag(), true);
|
||||||
|
verifyNoMoreInteractions(docker.image());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildInvokesBuilderAndPublishesImage() 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);
|
||||||
|
builder.build(request);
|
||||||
|
assertThat(out.toString()).contains("Running creator");
|
||||||
|
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
|
||||||
|
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
|
||||||
|
verify(docker.image()).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()).load(archive.capture(), any());
|
||||||
|
verify(docker.image()).remove(archive.getValue().getTag(), true);
|
||||||
|
verifyNoMoreInteractions(docker.image());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -103,11 +141,11 @@ class BuilderTests {
|
||||||
DockerApi docker = mockDockerApi();
|
DockerApi docker = mockDockerApi();
|
||||||
Image builderImage = loadImage("image-with-no-run-image-tag.json");
|
Image builderImage = loadImage("image-with-no-run-image-tag.json");
|
||||||
Image runImage = loadImage("run-image.json");
|
Image runImage = loadImage("run-image.json");
|
||||||
given(docker.image().pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), any()))
|
given(docker.image().pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(builderImage));
|
.willAnswer(withPulledImage(builderImage));
|
||||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:latest")), any()))
|
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:latest")), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(runImage));
|
.willAnswer(withPulledImage(runImage));
|
||||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||||
BuildRequest request = getTestRequest().withBuilder(ImageReference.of("gcr.io/paketo-buildpacks/builder"));
|
BuildRequest request = getTestRequest().withBuilder(ImageReference.of("gcr.io/paketo-buildpacks/builder"));
|
||||||
builder.build(request);
|
builder.build(request);
|
||||||
assertThat(out.toString()).contains("Running creator");
|
assertThat(out.toString()).contains("Running creator");
|
||||||
|
|
@ -123,12 +161,12 @@ class BuilderTests {
|
||||||
DockerApi docker = mockDockerApi();
|
DockerApi docker = mockDockerApi();
|
||||||
Image builderImage = loadImage("image-with-run-image-digest.json");
|
Image builderImage = loadImage("image-with-run-image-digest.json");
|
||||||
Image runImage = loadImage("run-image.json");
|
Image runImage = loadImage("run-image.json");
|
||||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(builderImage));
|
.willAnswer(withPulledImage(builderImage));
|
||||||
given(docker.image().pull(eq(ImageReference.of(
|
given(docker.image().pull(eq(ImageReference.of(
|
||||||
"docker.io/cloudfoundry/run@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d")),
|
"docker.io/cloudfoundry/run@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d")),
|
||||||
any())).willAnswer(withPulledImage(runImage));
|
any(), isNull())).willAnswer(withPulledImage(runImage));
|
||||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||||
BuildRequest request = getTestRequest();
|
BuildRequest request = getTestRequest();
|
||||||
builder.build(request);
|
builder.build(request);
|
||||||
assertThat(out.toString()).contains("Running creator");
|
assertThat(out.toString()).contains("Running creator");
|
||||||
|
|
@ -144,11 +182,11 @@ class BuilderTests {
|
||||||
DockerApi docker = mockDockerApi();
|
DockerApi docker = mockDockerApi();
|
||||||
Image builderImage = loadImage("image.json");
|
Image builderImage = loadImage("image.json");
|
||||||
Image runImage = loadImage("run-image.json");
|
Image runImage = loadImage("run-image.json");
|
||||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(builderImage));
|
.willAnswer(withPulledImage(builderImage));
|
||||||
given(docker.image().pull(eq(ImageReference.of("example.com/custom/run:latest")), any()))
|
given(docker.image().pull(eq(ImageReference.of("example.com/custom/run:latest")), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(runImage));
|
.willAnswer(withPulledImage(runImage));
|
||||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||||
BuildRequest request = getTestRequest().withRunImage(ImageReference.of("example.com/custom/run:latest"));
|
BuildRequest request = getTestRequest().withRunImage(ImageReference.of("example.com/custom/run:latest"));
|
||||||
builder.build(request);
|
builder.build(request);
|
||||||
assertThat(out.toString()).contains("Running creator");
|
assertThat(out.toString()).contains("Running creator");
|
||||||
|
|
@ -164,15 +202,15 @@ class BuilderTests {
|
||||||
DockerApi docker = mockDockerApi();
|
DockerApi docker = mockDockerApi();
|
||||||
Image builderImage = loadImage("image.json");
|
Image builderImage = loadImage("image.json");
|
||||||
Image runImage = loadImage("run-image.json");
|
Image runImage = loadImage("run-image.json");
|
||||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(builderImage));
|
.willAnswer(withPulledImage(builderImage));
|
||||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any()))
|
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(runImage));
|
.willAnswer(withPulledImage(runImage));
|
||||||
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME))))
|
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME))))
|
||||||
.willReturn(builderImage);
|
.willReturn(builderImage);
|
||||||
given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb"))))
|
given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb"))))
|
||||||
.willReturn(runImage);
|
.willReturn(runImage);
|
||||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||||
BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.NEVER);
|
BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.NEVER);
|
||||||
builder.build(request);
|
builder.build(request);
|
||||||
assertThat(out.toString()).contains("Running creator");
|
assertThat(out.toString()).contains("Running creator");
|
||||||
|
|
@ -190,15 +228,15 @@ class BuilderTests {
|
||||||
DockerApi docker = mockDockerApi();
|
DockerApi docker = mockDockerApi();
|
||||||
Image builderImage = loadImage("image.json");
|
Image builderImage = loadImage("image.json");
|
||||||
Image runImage = loadImage("run-image.json");
|
Image runImage = loadImage("run-image.json");
|
||||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(builderImage));
|
.willAnswer(withPulledImage(builderImage));
|
||||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any()))
|
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(runImage));
|
.willAnswer(withPulledImage(runImage));
|
||||||
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME))))
|
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME))))
|
||||||
.willReturn(builderImage);
|
.willReturn(builderImage);
|
||||||
given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb"))))
|
given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb"))))
|
||||||
.willReturn(runImage);
|
.willReturn(runImage);
|
||||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||||
BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.ALWAYS);
|
BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.ALWAYS);
|
||||||
builder.build(request);
|
builder.build(request);
|
||||||
assertThat(out.toString()).contains("Running creator");
|
assertThat(out.toString()).contains("Running creator");
|
||||||
|
|
@ -206,7 +244,7 @@ class BuilderTests {
|
||||||
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
|
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
|
||||||
verify(docker.image()).load(archive.capture(), any());
|
verify(docker.image()).load(archive.capture(), any());
|
||||||
verify(docker.image()).remove(archive.getValue().getTag(), true);
|
verify(docker.image()).remove(archive.getValue().getTag(), true);
|
||||||
verify(docker.image(), times(2)).pull(any(), any());
|
verify(docker.image(), times(2)).pull(any(), any(), isNull());
|
||||||
verify(docker.image(), never()).inspect(any());
|
verify(docker.image(), never()).inspect(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,9 +254,9 @@ class BuilderTests {
|
||||||
DockerApi docker = mockDockerApi();
|
DockerApi docker = mockDockerApi();
|
||||||
Image builderImage = loadImage("image.json");
|
Image builderImage = loadImage("image.json");
|
||||||
Image runImage = loadImage("run-image.json");
|
Image runImage = loadImage("run-image.json");
|
||||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(builderImage));
|
.willAnswer(withPulledImage(builderImage));
|
||||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any()))
|
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(runImage));
|
.willAnswer(withPulledImage(runImage));
|
||||||
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)))).willThrow(
|
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)))).willThrow(
|
||||||
new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null))
|
new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null))
|
||||||
|
|
@ -226,7 +264,7 @@ class BuilderTests {
|
||||||
given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))).willThrow(
|
given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))).willThrow(
|
||||||
new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null))
|
new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null))
|
||||||
.willReturn(runImage);
|
.willReturn(runImage);
|
||||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||||
BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.IF_NOT_PRESENT);
|
BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.IF_NOT_PRESENT);
|
||||||
builder.build(request);
|
builder.build(request);
|
||||||
assertThat(out.toString()).contains("Running creator");
|
assertThat(out.toString()).contains("Running creator");
|
||||||
|
|
@ -235,7 +273,7 @@ class BuilderTests {
|
||||||
verify(docker.image()).load(archive.capture(), any());
|
verify(docker.image()).load(archive.capture(), any());
|
||||||
verify(docker.image()).remove(archive.getValue().getTag(), true);
|
verify(docker.image()).remove(archive.getValue().getTag(), true);
|
||||||
verify(docker.image(), times(2)).inspect(any());
|
verify(docker.image(), times(2)).inspect(any());
|
||||||
verify(docker.image(), times(2)).pull(any(), any());
|
verify(docker.image(), times(2)).pull(any(), any(), isNull());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -244,11 +282,11 @@ class BuilderTests {
|
||||||
DockerApi docker = mockDockerApi();
|
DockerApi docker = mockDockerApi();
|
||||||
Image builderImage = loadImage("image.json");
|
Image builderImage = loadImage("image.json");
|
||||||
Image runImage = loadImage("run-image-with-bad-stack.json");
|
Image runImage = loadImage("run-image-with-bad-stack.json");
|
||||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(builderImage));
|
.willAnswer(withPulledImage(builderImage));
|
||||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any()))
|
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(runImage));
|
.willAnswer(withPulledImage(runImage));
|
||||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||||
BuildRequest request = getTestRequest();
|
BuildRequest request = getTestRequest();
|
||||||
assertThatIllegalStateException().isThrownBy(() -> builder.build(request)).withMessage(
|
assertThatIllegalStateException().isThrownBy(() -> builder.build(request)).withMessage(
|
||||||
"Run image stack 'org.cloudfoundry.stacks.cfwindowsfs3' does not match builder stack 'io.buildpacks.stacks.bionic'");
|
"Run image stack 'org.cloudfoundry.stacks.cfwindowsfs3' does not match builder stack 'io.buildpacks.stacks.bionic'");
|
||||||
|
|
@ -260,11 +298,11 @@ class BuilderTests {
|
||||||
DockerApi docker = mockDockerApiLifecycleError();
|
DockerApi docker = mockDockerApiLifecycleError();
|
||||||
Image builderImage = loadImage("image.json");
|
Image builderImage = loadImage("image.json");
|
||||||
Image runImage = loadImage("run-image.json");
|
Image runImage = loadImage("run-image.json");
|
||||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(builderImage));
|
.willAnswer(withPulledImage(builderImage));
|
||||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any()))
|
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
|
||||||
.willAnswer(withPulledImage(runImage));
|
.willAnswer(withPulledImage(runImage));
|
||||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||||
BuildRequest request = getTestRequest();
|
BuildRequest request = getTestRequest();
|
||||||
assertThatExceptionOfType(BuilderException.class).isThrownBy(() -> builder.build(request))
|
assertThatExceptionOfType(BuilderException.class).isThrownBy(() -> builder.build(request))
|
||||||
.withMessage("Builder lifecycle 'creator' failed with status code 9");
|
.withMessage("Builder lifecycle 'creator' failed with status code 9");
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.isNull;
|
||||||
import static org.mockito.BDDMockito.given;
|
import static org.mockito.BDDMockito.given;
|
||||||
import static org.mockito.Mockito.inOrder;
|
import static org.mockito.Mockito.inOrder;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
@ -127,6 +128,9 @@ class DockerApiTests {
|
||||||
@Mock
|
@Mock
|
||||||
private UpdateListener<PullImageUpdateEvent> pullListener;
|
private UpdateListener<PullImageUpdateEvent> pullListener;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private UpdateListener<PushImageUpdateEvent> pushListener;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private UpdateListener<LoadImageUpdateEvent> loadListener;
|
private UpdateListener<LoadImageUpdateEvent> loadListener;
|
||||||
|
|
||||||
|
|
@ -156,7 +160,7 @@ class DockerApiTests {
|
||||||
URI createUri = new URI(IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase");
|
URI createUri = new URI(IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase");
|
||||||
String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30";
|
String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30";
|
||||||
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder@sha256:" + imageHash + "/json");
|
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder@sha256:" + imageHash + "/json");
|
||||||
given(http().post(createUri)).willReturn(responseOf("pull-stream.json"));
|
given(http().post(eq(createUri), isNull())).willReturn(responseOf("pull-stream.json"));
|
||||||
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
|
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
|
||||||
Image image = this.api.pull(reference, this.pullListener);
|
Image image = this.api.pull(reference, this.pullListener);
|
||||||
assertThat(image.getLayers()).hasSize(46);
|
assertThat(image.getLayers()).hasSize(46);
|
||||||
|
|
@ -166,6 +170,57 @@ class DockerApiTests {
|
||||||
ordered.verify(this.pullListener).onFinish();
|
ordered.verify(this.pullListener).onFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pullWithRegistryAuthPullsImageAndProducesEvents() throws Exception {
|
||||||
|
ImageReference reference = ImageReference.of("gcr.io/paketo-buildpacks/builder:base");
|
||||||
|
URI createUri = new URI(IMAGES_URL + "/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase");
|
||||||
|
String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30";
|
||||||
|
URI imageUri = new URI(IMAGES_URL + "/gcr.io/paketo-buildpacks/builder@sha256:" + imageHash + "/json");
|
||||||
|
given(http().post(eq(createUri), eq("auth token"))).willReturn(responseOf("pull-stream.json"));
|
||||||
|
given(http().get(imageUri)).willReturn(responseOf("type/image.json"));
|
||||||
|
Image image = this.api.pull(reference, this.pullListener, "auth token");
|
||||||
|
assertThat(image.getLayers()).hasSize(46);
|
||||||
|
InOrder ordered = inOrder(this.pullListener);
|
||||||
|
ordered.verify(this.pullListener).onStart();
|
||||||
|
ordered.verify(this.pullListener, times(595)).onUpdate(any());
|
||||||
|
ordered.verify(this.pullListener).onFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushWhenReferenceIsNullThrowsException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> this.api.push(null, this.pushListener, null))
|
||||||
|
.withMessage("Reference must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushWhenListenerIsNullThrowsException() {
|
||||||
|
assertThatIllegalArgumentException()
|
||||||
|
.isThrownBy(() -> this.api.push(ImageReference.of("ubuntu"), null, null))
|
||||||
|
.withMessage("Listener must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushPushesImageAndProducesEvents() throws Exception {
|
||||||
|
ImageReference reference = ImageReference.of("localhost:5000/ubuntu");
|
||||||
|
URI pushUri = new URI(IMAGES_URL + "/localhost:5000/ubuntu/push");
|
||||||
|
given(http().post(pushUri, "auth token")).willReturn(responseOf("push-stream.json"));
|
||||||
|
this.api.push(reference, this.pushListener, "auth token");
|
||||||
|
InOrder ordered = inOrder(this.pushListener);
|
||||||
|
ordered.verify(this.pushListener).onStart();
|
||||||
|
ordered.verify(this.pushListener, times(44)).onUpdate(any());
|
||||||
|
ordered.verify(this.pushListener).onFinish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pushWithErrorInStreamThrowsException() throws Exception {
|
||||||
|
ImageReference reference = ImageReference.of("localhost:5000/ubuntu");
|
||||||
|
URI pushUri = new URI(IMAGES_URL + "/localhost:5000/ubuntu/push");
|
||||||
|
given(http().post(pushUri, "auth token")).willReturn(responseOf("push-stream-with-error.json"));
|
||||||
|
assertThatIllegalStateException()
|
||||||
|
.isThrownBy(() -> this.api.push(reference, this.pushListener, "auth token"))
|
||||||
|
.withMessageContaining("test message");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void loadWhenArchiveIsNullThrowsException() {
|
void loadWhenArchiveIsNullThrowsException() {
|
||||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.load(null, UpdateListener.none()))
|
assertThatIllegalArgumentException().isThrownBy(() -> this.api.load(null, UpdateListener.none()))
|
||||||
|
|
|
||||||
|
|
@ -26,17 +26,18 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
* Tests for {@link LoadImageUpdateEvent}.
|
* Tests for {@link LoadImageUpdateEvent}.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Scott Frederick
|
||||||
*/
|
*/
|
||||||
class LoadImageUpdateEventTests extends ProgressUpdateEventTests {
|
class LoadImageUpdateEventTests extends ProgressUpdateEventTests<LoadImageUpdateEvent> {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getStreamReturnsStream() {
|
void getStreamReturnsStream() {
|
||||||
LoadImageUpdateEvent event = (LoadImageUpdateEvent) createEvent();
|
LoadImageUpdateEvent event = createEvent();
|
||||||
assertThat(event.getStream()).isEqualTo("stream");
|
assertThat(event.getStream()).isEqualTo("stream");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ProgressUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) {
|
protected LoadImageUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) {
|
||||||
return new LoadImageUpdateEvent("stream", status, progressDetail, progress);
|
return new LoadImageUpdateEvent("stream", status, progressDetail, progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
* Tests for {@link ProgressUpdateEvent}.
|
* Tests for {@link ProgressUpdateEvent}.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Scott Frederick
|
||||||
*/
|
*/
|
||||||
abstract class ProgressUpdateEventTests {
|
abstract class ProgressUpdateEventTests<E extends ProgressUpdateEvent> {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getStatusReturnsStatus() {
|
void getStatusReturnsStatus() {
|
||||||
|
|
@ -66,10 +67,10 @@ abstract class ProgressUpdateEventTests {
|
||||||
assertThat(ProgressDetail.isEmpty(detail)).isFalse();
|
assertThat(ProgressDetail.isEmpty(detail)).isFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ProgressUpdateEvent createEvent() {
|
protected E createEvent() {
|
||||||
return createEvent("status", new ProgressDetail(1, 2), "progress");
|
return createEvent("status", new ProgressDetail(1, 2), "progress");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract ProgressUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress);
|
protected abstract E createEvent(String status, ProgressDetail progressDetail, String progress);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,17 +26,18 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
* Tests for {@link PullImageUpdateEvent}.
|
* Tests for {@link PullImageUpdateEvent}.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Scott Frederick
|
||||||
*/
|
*/
|
||||||
class PullImageUpdateEventTests extends ProgressUpdateEventTests {
|
class PullImageUpdateEventTests extends ProgressUpdateEventTests<PullImageUpdateEvent> {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getIdReturnsId() {
|
void getIdReturnsId() {
|
||||||
PullImageUpdateEvent event = (PullImageUpdateEvent) createEvent();
|
PullImageUpdateEvent event = createEvent();
|
||||||
assertThat(event.getId()).isEqualTo("id");
|
assertThat(event.getId()).isEqualTo("id");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected ProgressUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) {
|
protected PullImageUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) {
|
||||||
return new PullImageUpdateEvent("id", status, progressDetail, progress);
|
return new PullImageUpdateEvent("id", status, progressDetail, progress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2020 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.buildpack.platform.docker;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.ProgressUpdateEvent.ProgressDetail;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link PushImageUpdateEvent}.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
|
*/
|
||||||
|
class PushImageUpdateEventTests extends ProgressUpdateEventTests<PushImageUpdateEvent> {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getIdReturnsId() {
|
||||||
|
PushImageUpdateEvent event = createEvent();
|
||||||
|
assertThat(event.getId()).isEqualTo("id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getErrorReturnsErrorDetail() {
|
||||||
|
PushImageUpdateEvent event = new PushImageUpdateEvent(null, null, null, null,
|
||||||
|
new PushImageUpdateEvent.ErrorDetail("test message"));
|
||||||
|
assertThat(event.getErrorDetail().getMessage()).isEqualTo("test message");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PushImageUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) {
|
||||||
|
return new PushImageUpdateEvent("id", status, progressDetail, progress, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
|
@ -33,13 +34,14 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
* Tests for {@link TotalProgressPullListener}.
|
* Tests for {@link TotalProgressPullListener}.
|
||||||
*
|
*
|
||||||
* @author Phillip Webb
|
* @author Phillip Webb
|
||||||
|
* @author Scott Frederick
|
||||||
*/
|
*/
|
||||||
class TotalProgressPullListenerTests extends AbstractJsonTests {
|
class TotalProgressListenerTests extends AbstractJsonTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void totalProgress() throws Exception {
|
void totalProgress() throws Exception {
|
||||||
List<Integer> progress = new ArrayList<>();
|
List<Integer> progress = new ArrayList<>();
|
||||||
TotalProgressPullListener listener = new TotalProgressPullListener((event) -> progress.add(event.getPercent()));
|
TestTotalProgressListener listener = new TestTotalProgressListener((event) -> progress.add(event.getPercent()));
|
||||||
run(listener);
|
run(listener);
|
||||||
int last = 0;
|
int last = 0;
|
||||||
for (Integer update : progress) {
|
for (Integer update : progress) {
|
||||||
|
|
@ -52,26 +54,25 @@ class TotalProgressPullListenerTests extends AbstractJsonTests {
|
||||||
@Test
|
@Test
|
||||||
@Disabled("For visual inspection")
|
@Disabled("For visual inspection")
|
||||||
void totalProgressUpdatesSmoothly() throws Exception {
|
void totalProgressUpdatesSmoothly() throws Exception {
|
||||||
TestTotalProgressPullListener listener = new TestTotalProgressPullListener(
|
TestTotalProgressListener listener = new TestTotalProgressListener(new TotalProgressBar("Pulling layers:"));
|
||||||
new TotalProgressBar("Pulling layers:"));
|
|
||||||
run(listener);
|
run(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void run(TotalProgressPullListener listener) throws IOException {
|
private void run(TestTotalProgressListener listener) throws IOException {
|
||||||
JsonStream jsonStream = new JsonStream(getObjectMapper());
|
JsonStream jsonStream = new JsonStream(getObjectMapper());
|
||||||
listener.onStart();
|
listener.onStart();
|
||||||
jsonStream.get(getContent("pull-stream.json"), PullImageUpdateEvent.class, listener::onUpdate);
|
jsonStream.get(getContent("pull-stream.json"), TestImageUpdateEvent.class, listener::onUpdate);
|
||||||
listener.onFinish();
|
listener.onFinish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class TestTotalProgressPullListener extends TotalProgressPullListener {
|
private static class TestTotalProgressListener extends TotalProgressListener<TestImageUpdateEvent> {
|
||||||
|
|
||||||
TestTotalProgressPullListener(Consumer<TotalProgressEvent> consumer) {
|
TestTotalProgressListener(Consumer<TotalProgressEvent> consumer) {
|
||||||
super(consumer);
|
super(consumer, new String[] { "Pulling", "Downloading", "Extracting" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onUpdate(PullImageUpdateEvent event) {
|
public void onUpdate(TestImageUpdateEvent event) {
|
||||||
super.onUpdate(event);
|
super.onUpdate(event);
|
||||||
try {
|
try {
|
||||||
Thread.sleep(10);
|
Thread.sleep(10);
|
||||||
|
|
@ -82,4 +83,13 @@ class TotalProgressPullListenerTests extends AbstractJsonTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class TestImageUpdateEvent extends ImageProgressUpdateEvent {
|
||||||
|
|
||||||
|
@JsonCreator
|
||||||
|
TestImageUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress) {
|
||||||
|
super(id, status, progressDetail, progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -31,14 +31,14 @@ public class DockerConfigurationTests {
|
||||||
@Test
|
@Test
|
||||||
void createDockerConfigurationWithDefaults() {
|
void createDockerConfigurationWithDefaults() {
|
||||||
DockerConfiguration configuration = new DockerConfiguration();
|
DockerConfiguration configuration = new DockerConfiguration();
|
||||||
assertThat(configuration.getRegistryAuthentication()).isNull();
|
assertThat(configuration.getBuilderRegistryAuthentication()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createDockerConfigurationWithUserAuth() {
|
void createDockerConfigurationWithUserAuth() {
|
||||||
DockerConfiguration configuration = new DockerConfiguration().withRegistryUserAuthentication("user", "secret",
|
DockerConfiguration configuration = new DockerConfiguration().withBuilderRegistryUserAuthentication("user",
|
||||||
"https://docker.example.com", "docker@example.com");
|
"secret", "https://docker.example.com", "docker@example.com");
|
||||||
DockerRegistryAuthentication auth = configuration.getRegistryAuthentication();
|
DockerRegistryAuthentication auth = configuration.getBuilderRegistryAuthentication();
|
||||||
assertThat(auth).isNotNull();
|
assertThat(auth).isNotNull();
|
||||||
assertThat(auth).isInstanceOf(DockerRegistryUserAuthentication.class);
|
assertThat(auth).isInstanceOf(DockerRegistryUserAuthentication.class);
|
||||||
DockerRegistryUserAuthentication userAuth = (DockerRegistryUserAuthentication) auth;
|
DockerRegistryUserAuthentication userAuth = (DockerRegistryUserAuthentication) auth;
|
||||||
|
|
@ -50,8 +50,8 @@ public class DockerConfigurationTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createDockerConfigurationWithTokenAuth() {
|
void createDockerConfigurationWithTokenAuth() {
|
||||||
DockerConfiguration configuration = new DockerConfiguration().withRegistryTokenAuthentication("token");
|
DockerConfiguration configuration = new DockerConfiguration().withBuilderRegistryTokenAuthentication("token");
|
||||||
DockerRegistryAuthentication auth = configuration.getRegistryAuthentication();
|
DockerRegistryAuthentication auth = configuration.getBuilderRegistryAuthentication();
|
||||||
assertThat(auth).isNotNull();
|
assertThat(auth).isNotNull();
|
||||||
assertThat(auth).isInstanceOf(DockerRegistryTokenAuthentication.class);
|
assertThat(auth).isInstanceOf(DockerRegistryTokenAuthentication.class);
|
||||||
DockerRegistryTokenAuthentication tokenAuth = (DockerRegistryTokenAuthentication) auth;
|
DockerRegistryTokenAuthentication tokenAuth = (DockerRegistryTokenAuthentication) auth;
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,15 @@ import org.springframework.util.StreamUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link DockerRegistryTokenAuthentication}.
|
* Tests for {@link DockerRegistryTokenAuthentication}.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
*/
|
*/
|
||||||
class DockerRegistryTokenAuthenticationTests extends AbstractJsonTests {
|
class DockerRegistryTokenAuthenticationTests extends AbstractJsonTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
|
void createAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
|
||||||
DockerRegistryTokenAuthentication auth = new DockerRegistryTokenAuthentication("tokenvalue");
|
DockerRegistryTokenAuthentication auth = new DockerRegistryTokenAuthentication("tokenvalue");
|
||||||
String header = auth.createAuthHeader();
|
String header = auth.getAuthHeader();
|
||||||
String expectedJson = StreamUtils.copyToString(getContent("auth-token.json"), StandardCharsets.UTF_8);
|
String expectedJson = StreamUtils.copyToString(getContent("auth-token.json"), StandardCharsets.UTF_8);
|
||||||
JSONAssert.assertEquals(expectedJson, new String(Base64Utils.decodeFromUrlSafeString(header)), false);
|
JSONAssert.assertEquals(expectedJson, new String(Base64Utils.decodeFromUrlSafeString(header)), false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ import org.springframework.util.StreamUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link DockerRegistryUserAuthentication}.
|
* Tests for {@link DockerRegistryUserAuthentication}.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
*/
|
*/
|
||||||
class DockerRegistryUserAuthenticationTests extends AbstractJsonTests {
|
class DockerRegistryUserAuthenticationTests extends AbstractJsonTests {
|
||||||
|
|
||||||
|
|
@ -36,13 +38,13 @@ class DockerRegistryUserAuthenticationTests extends AbstractJsonTests {
|
||||||
void createMinimalAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
|
void createMinimalAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
|
||||||
DockerRegistryUserAuthentication auth = new DockerRegistryUserAuthentication("user", "secret",
|
DockerRegistryUserAuthentication auth = new DockerRegistryUserAuthentication("user", "secret",
|
||||||
"https://docker.example.com", "docker@example.com");
|
"https://docker.example.com", "docker@example.com");
|
||||||
JSONAssert.assertEquals(jsonContent("auth-user-full.json"), decoded(auth.createAuthHeader()), false);
|
JSONAssert.assertEquals(jsonContent("auth-user-full.json"), decoded(auth.getAuthHeader()), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createFullAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
|
void createFullAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
|
||||||
DockerRegistryUserAuthentication auth = new DockerRegistryUserAuthentication("user", "secret", null, null);
|
DockerRegistryUserAuthentication auth = new DockerRegistryUserAuthentication("user", "secret", null, null);
|
||||||
JSONAssert.assertEquals(jsonContent("auth-user-minimal.json"), decoded(auth.createAuthHeader()), false);
|
JSONAssert.assertEquals(jsonContent("auth-user-minimal.json"), decoded(auth.getAuthHeader()), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String jsonContent(String s) throws IOException {
|
private String jsonContent(String s) throws IOException {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
import org.apache.http.Header;
|
|
||||||
import org.apache.http.HttpEntity;
|
import org.apache.http.HttpEntity;
|
||||||
import org.apache.http.HttpEntityEnclosingRequest;
|
import org.apache.http.HttpEntityEnclosingRequest;
|
||||||
import org.apache.http.HttpHeaders;
|
import org.apache.http.HttpHeaders;
|
||||||
|
|
@ -44,9 +43,7 @@ import org.mockito.Captor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response;
|
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response;
|
||||||
import org.springframework.util.Base64Utils;
|
|
||||||
import org.springframework.util.StreamUtils;
|
import org.springframework.util.StreamUtils;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
@ -123,6 +120,37 @@ class HttpClientTransportTests {
|
||||||
assertThat(request).isInstanceOf(HttpPost.class);
|
assertThat(request).isInstanceOf(HttpPost.class);
|
||||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||||
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull();
|
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull();
|
||||||
|
assertThat(request.getFirstHeader(HttpClientTransport.REGISTRY_AUTH_HEADER)).isNull();
|
||||||
|
assertThat(response.getContent()).isSameAs(this.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void postWithRegistryAuthShouldExecuteHttpPostWithHeader() throws Exception {
|
||||||
|
givenClientWillReturnResponse();
|
||||||
|
given(this.entity.getContent()).willReturn(this.content);
|
||||||
|
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||||
|
Response response = this.http.post(this.uri, "auth token");
|
||||||
|
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
||||||
|
HttpUriRequest request = this.requestCaptor.getValue();
|
||||||
|
assertThat(request).isInstanceOf(HttpPost.class);
|
||||||
|
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||||
|
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull();
|
||||||
|
assertThat(request.getFirstHeader(HttpClientTransport.REGISTRY_AUTH_HEADER).getValue()).isEqualTo("auth token");
|
||||||
|
assertThat(response.getContent()).isSameAs(this.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void postWithEmptyRegistryAuthShouldExecuteHttpPostWithoutHeader() throws Exception {
|
||||||
|
givenClientWillReturnResponse();
|
||||||
|
given(this.entity.getContent()).willReturn(this.content);
|
||||||
|
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||||
|
Response response = this.http.post(this.uri, "");
|
||||||
|
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
||||||
|
HttpUriRequest request = this.requestCaptor.getValue();
|
||||||
|
assertThat(request).isInstanceOf(HttpPost.class);
|
||||||
|
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||||
|
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull();
|
||||||
|
assertThat(request.getFirstHeader(HttpClientTransport.REGISTRY_AUTH_HEADER)).isNull();
|
||||||
assertThat(response.getContent()).isSameAs(this.content);
|
assertThat(response.getContent()).isSameAs(this.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,47 +265,6 @@ class HttpClientTransportTests {
|
||||||
.satisfies((ex) -> assertThat(ex.getMessage()).contains("test IO exception"));
|
.satisfies((ex) -> assertThat(ex.getMessage()).contains("test IO exception"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void getWithDockerRegistryUserAuthWillSendAuthHeader() throws IOException {
|
|
||||||
DockerConfiguration dockerConfiguration = new DockerConfiguration().withRegistryUserAuthentication("user",
|
|
||||||
"secret", "https://docker.example.com", "docker@example.com");
|
|
||||||
this.http = new TestHttpClientTransport(this.client, dockerConfiguration);
|
|
||||||
givenClientWillReturnResponse();
|
|
||||||
given(this.entity.getContent()).willReturn(this.content);
|
|
||||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
|
||||||
Response response = this.http.get(this.uri);
|
|
||||||
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
|
||||||
HttpUriRequest request = this.requestCaptor.getValue();
|
|
||||||
assertThat(request).isInstanceOf(HttpGet.class);
|
|
||||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
|
||||||
Header[] registryAuthHeaders = request.getHeaders("X-Registry-Auth");
|
|
||||||
assertThat(registryAuthHeaders).isNotNull();
|
|
||||||
assertThat(new String(Base64Utils.decodeFromString(registryAuthHeaders[0].getValue())))
|
|
||||||
.contains("\"username\" : \"user\"").contains("\"password\" : \"secret\"")
|
|
||||||
.contains("\"email\" : \"docker@example.com\"")
|
|
||||||
.contains("\"serveraddress\" : \"https://docker.example.com\"");
|
|
||||||
assertThat(response.getContent()).isSameAs(this.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void getWithDockerRegistryTokenAuthWillSendAuthHeader() throws IOException {
|
|
||||||
DockerConfiguration dockerConfiguration = new DockerConfiguration().withRegistryTokenAuthentication("token");
|
|
||||||
this.http = new TestHttpClientTransport(this.client, dockerConfiguration);
|
|
||||||
givenClientWillReturnResponse();
|
|
||||||
given(this.entity.getContent()).willReturn(this.content);
|
|
||||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
|
||||||
Response response = this.http.get(this.uri);
|
|
||||||
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
|
||||||
HttpUriRequest request = this.requestCaptor.getValue();
|
|
||||||
assertThat(request).isInstanceOf(HttpGet.class);
|
|
||||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
|
||||||
Header[] registryAuthHeaders = request.getHeaders("X-Registry-Auth");
|
|
||||||
assertThat(registryAuthHeaders).isNotNull();
|
|
||||||
assertThat(new String(Base64Utils.decodeFromString(registryAuthHeaders[0].getValue())))
|
|
||||||
.contains("\"identitytoken\" : \"token\"");
|
|
||||||
assertThat(response.getContent()).isSameAs(this.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String writeToString(HttpEntity entity) throws IOException {
|
private String writeToString(HttpEntity entity) throws IOException {
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
entity.writeTo(out);
|
entity.writeTo(out);
|
||||||
|
|
@ -296,11 +283,7 @@ class HttpClientTransportTests {
|
||||||
static class TestHttpClientTransport extends HttpClientTransport {
|
static class TestHttpClientTransport extends HttpClientTransport {
|
||||||
|
|
||||||
protected TestHttpClientTransport(CloseableHttpClient client) {
|
protected TestHttpClientTransport(CloseableHttpClient client) {
|
||||||
super(client, HttpHost.create("docker://localhost"), null);
|
super(client, HttpHost.create("docker://localhost"));
|
||||||
}
|
|
||||||
|
|
||||||
protected TestHttpClientTransport(CloseableHttpClient client, DockerConfiguration dockerConfiguration) {
|
|
||||||
super(client, HttpHost.create("docker://localhost"), dockerConfiguration.getRegistryAuthentication());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost;
|
||||||
import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory;
|
import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
@ -52,7 +53,7 @@ class RemoteHttpClientTransportTests {
|
||||||
@Test
|
@Test
|
||||||
void createIfPossibleWhenDockerHostIsNotSetReturnsNull() {
|
void createIfPossibleWhenDockerHostIsNotSetReturnsNull() {
|
||||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
||||||
this.dockerConfiguration);
|
new DockerHost(null, false, null));
|
||||||
assertThat(transport).isNull();
|
assertThat(transport).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,8 +68,7 @@ class RemoteHttpClientTransportTests {
|
||||||
String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath()
|
String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath()
|
||||||
.toString();
|
.toString();
|
||||||
this.environment.put("DOCKER_HOST", dummySocketFilePath);
|
this.environment.put("DOCKER_HOST", dummySocketFilePath);
|
||||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, null);
|
||||||
this.dockerConfiguration);
|
|
||||||
assertThat(transport).isNull();
|
assertThat(transport).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,22 +77,21 @@ class RemoteHttpClientTransportTests {
|
||||||
String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath()
|
String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath()
|
||||||
.toString();
|
.toString();
|
||||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
||||||
this.dockerConfiguration.withHost(dummySocketFilePath, false, null));
|
new DockerHost(dummySocketFilePath, false, null));
|
||||||
assertThat(transport).isNull();
|
assertThat(transport).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createIfPossibleWhenDockerHostInEnvironmentIsAddressReturnsTransport() {
|
void createIfPossibleWhenDockerHostInEnvironmentIsAddressReturnsTransport() {
|
||||||
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
|
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
|
||||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, null);
|
||||||
this.dockerConfiguration);
|
|
||||||
assertThat(transport).isNotNull();
|
assertThat(transport).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createIfPossibleWhenDockerHostInConfigurationIsAddressReturnsTransport() {
|
void createIfPossibleWhenDockerHostInConfigurationIsAddressReturnsTransport() {
|
||||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
||||||
this.dockerConfiguration.withHost("tcp://192.168.1.2:2376", false, null));
|
new DockerHost("tcp://192.168.1.2:2376", false, null));
|
||||||
assertThat(transport).isNotNull();
|
assertThat(transport).isNotNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,8 +99,8 @@ class RemoteHttpClientTransportTests {
|
||||||
void createIfPossibleWhenTlsVerifyInEnvironmentWithMissingCertPathThrowsException() {
|
void createIfPossibleWhenTlsVerifyInEnvironmentWithMissingCertPathThrowsException() {
|
||||||
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
|
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
|
||||||
this.environment.put("DOCKER_TLS_VERIFY", "1");
|
this.environment.put("DOCKER_TLS_VERIFY", "1");
|
||||||
assertThatIllegalArgumentException().isThrownBy(
|
assertThatIllegalArgumentException()
|
||||||
() -> RemoteHttpClientTransport.createIfPossible(this.environment::get, this.dockerConfiguration))
|
.isThrownBy(() -> RemoteHttpClientTransport.createIfPossible(this.environment::get, null))
|
||||||
.withMessageContaining("Docker host TLS verification requires trust material");
|
.withMessageContaining("Docker host TLS verification requires trust material");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,15 +108,14 @@ class RemoteHttpClientTransportTests {
|
||||||
void createIfPossibleWhenTlsVerifyInConfigurationWithMissingCertPathThrowsException() {
|
void createIfPossibleWhenTlsVerifyInConfigurationWithMissingCertPathThrowsException() {
|
||||||
assertThatIllegalArgumentException()
|
assertThatIllegalArgumentException()
|
||||||
.isThrownBy(() -> RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
.isThrownBy(() -> RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
||||||
this.dockerConfiguration.withHost("tcp://192.168.1.2:2376", true, null)))
|
new DockerHost("tcp://192.168.1.2:2376", true, null)))
|
||||||
.withMessageContaining("Docker host TLS verification requires trust material");
|
.withMessageContaining("Docker host TLS verification requires trust material");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createIfPossibleWhenNoTlsVerifyUsesHttp() {
|
void createIfPossibleWhenNoTlsVerifyUsesHttp() {
|
||||||
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
|
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
|
||||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, null);
|
||||||
this.dockerConfiguration);
|
|
||||||
assertThat(transport.getHost()).satisfies(hostOf("http", "192.168.1.2", 2376));
|
assertThat(transport.getHost()).satisfies(hostOf("http", "192.168.1.2", 2376));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,7 +127,7 @@ class RemoteHttpClientTransportTests {
|
||||||
SslContextFactory sslContextFactory = mock(SslContextFactory.class);
|
SslContextFactory sslContextFactory = mock(SslContextFactory.class);
|
||||||
given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault());
|
given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault());
|
||||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
||||||
this.dockerConfiguration, sslContextFactory);
|
this.dockerConfiguration.getHost(), sslContextFactory);
|
||||||
assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376));
|
assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -138,20 +136,11 @@ class RemoteHttpClientTransportTests {
|
||||||
SslContextFactory sslContextFactory = mock(SslContextFactory.class);
|
SslContextFactory sslContextFactory = mock(SslContextFactory.class);
|
||||||
given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault());
|
given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault());
|
||||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
||||||
this.dockerConfiguration.withHost("tcp://192.168.1.2:2376", true, "/test-cert-path"),
|
this.dockerConfiguration.withHost("tcp://192.168.1.2:2376", true, "/test-cert-path").getHost(),
|
||||||
sslContextFactory);
|
sslContextFactory);
|
||||||
assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376));
|
assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void createIfPossibleWithDockerConfigurationUserAuthReturnsTransport() {
|
|
||||||
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
|
|
||||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
|
||||||
new DockerConfiguration().withRegistryUserAuthentication("user", "secret", "http://docker.example.com",
|
|
||||||
"docker@example.com"));
|
|
||||||
assertThat(transport).isNotNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Consumer<HttpHost> hostOf(String scheme, String hostName, int port) {
|
private Consumer<HttpHost> hostOf(String scheme, String hostName, int port) {
|
||||||
return (host) -> {
|
return (host) -> {
|
||||||
assertThat(host).isNotNull();
|
assertThat(host).isNotNull();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"status":"The push refers to repository [localhost:5000/ubuntu]"
|
||||||
|
}
|
||||||
|
{"status":"Preparing","progressDetail":{},"id":"782f5f011dda"}
|
||||||
|
{"status":"Preparing","progressDetail":{},"id":"90ac32a0d9ab"}
|
||||||
|
{"status":"Preparing","progressDetail":{},"id":"d42a4fdf4b2a"}
|
||||||
|
{"errorDetail":{"message":"test message"},"error":"test error"}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"status":"The push refers to repository [localhost:5000/ubuntu]"
|
||||||
|
}
|
||||||
|
{"status":"Preparing","progressDetail":{},"id":"782f5f011dda"}
|
||||||
|
{"status":"Preparing","progressDetail":{},"id":"90ac32a0d9ab"}
|
||||||
|
{"status":"Preparing","progressDetail":{},"id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":512,"total":7},"progress":"[==================================================\u003e] 512B","id":"782f5f011dda"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":512,"total":811},"progress":"[===============================\u003e ] 512B/811B","id":"90ac32a0d9ab"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":3072,"total":7},"progress":"[==================================================\u003e] 3.072kB","id":"782f5f011dda"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":15360,"total":811},"progress":"[==================================================\u003e] 15.36kB","id":"90ac32a0d9ab"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":543232,"total":72874905},"progress":"[\u003e ] 543.2kB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushed","progressDetail":{},"id":"90ac32a0d9ab"}
|
||||||
|
{"status":"Pushed","progressDetail":{},"id":"782f5f011dda"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":2713600,"total":72874905},"progress":"[=\u003e ] 2.714MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":4870656,"total":72874905},"progress":"[===\u003e ] 4.871MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":7069184,"total":72874905},"progress":"[====\u003e ] 7.069MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":9238528,"total":72874905},"progress":"[======\u003e ] 9.239MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":11354112,"total":72874905},"progress":"[=======\u003e ] 11.35MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":13582336,"total":72874905},"progress":"[=========\u003e ] 13.58MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":16336248,"total":72874905},"progress":"[===========\u003e ] 16.34MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":19036160,"total":72874905},"progress":"[=============\u003e ] 19.04MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":21762560,"total":72874905},"progress":"[==============\u003e ] 21.76MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":24480256,"total":72874905},"progress":"[================\u003e ] 24.48MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":28756480,"total":72874905},"progress":"[===================\u003e ] 28.76MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":32001024,"total":72874905},"progress":"[=====================\u003e ] 32MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":34195456,"total":72874905},"progress":"[=======================\u003e ] 34.2MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":36393984,"total":72874905},"progress":"[========================\u003e ] 36.39MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":38587904,"total":72874905},"progress":"[==========================\u003e ] 38.59MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":41290752,"total":72874905},"progress":"[============================\u003e ] 41.29MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":43487744,"total":72874905},"progress":"[=============================\u003e ] 43.49MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":45683200,"total":72874905},"progress":"[===============================\u003e ] 45.68MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":48413184,"total":72874905},"progress":"[=================================\u003e ] 48.41MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":51119104,"total":72874905},"progress":"[===================================\u003e ] 51.12MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":53327360,"total":72874905},"progress":"[====================================\u003e ] 53.33MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":54964224,"total":72874905},"progress":"[=====================================\u003e ] 54.96MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":57169408,"total":72874905},"progress":"[=======================================\u003e ] 57.17MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":59355825,"total":72874905},"progress":"[========================================\u003e ] 59.36MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":62002592,"total":72874905},"progress":"[==========================================\u003e ] 62MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":64700928,"total":72874905},"progress":"[============================================\u003e ] 64.7MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":67435688,"total":72874905},"progress":"[==============================================\u003e ] 67.44MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":70095743,"total":72874905},"progress":"[================================================\u003e ] 70.1MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":72823808,"total":72874905},"progress":"[=================================================\u003e ] 72.82MB/72.87MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushing","progressDetail":{"current":75247104,"total":72874905},"progress":"[==================================================\u003e] 75.25MB","id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"Pushed","progressDetail":{},"id":"d42a4fdf4b2a"}
|
||||||
|
{"status":"latest: digest: sha256:2e70e9c81838224b5311970dbf7ed16802fbfe19e7a70b3cbfa3d7522aa285b4 size: 943"}
|
||||||
|
{"progressDetail":{},"aux":{"Tag":"latest","Digest":"sha256:2e70e9c81838224b5311970dbf7ed16802fbfe19e7a70b3cbfa3d7522aa285b4","Size":943}}
|
||||||
|
|
@ -41,6 +41,7 @@ dependencies {
|
||||||
testImplementation("org.assertj:assertj-core")
|
testImplementation("org.assertj:assertj-core")
|
||||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
testImplementation("org.mockito:mockito-core")
|
testImplementation("org.mockito:mockito-core")
|
||||||
|
testImplementation("org.testcontainers:junit-jupiter")
|
||||||
testImplementation("org.testcontainers:testcontainers")
|
testImplementation("org.testcontainers:testcontainers")
|
||||||
|
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||||
|
|
|
||||||
|
|
@ -56,11 +56,14 @@ For more details, see also <<build-image-example-docker,examples>>.
|
||||||
|
|
||||||
[[build-image-docker-registry]]
|
[[build-image-docker-registry]]
|
||||||
=== Docker Registry
|
=== Docker Registry
|
||||||
If the Docker images specified by the `builder` or `runImage` parameters are stored in a private Docker image registry that requires authentication, the authentication credentials can be provided using `docker.registry` properties.
|
If the Docker images specified by the `builder` or `runImage` properties are stored in a private Docker image registry that requires authentication, the authentication credentials can be provided using `docker.builderRegistry` properties.
|
||||||
Properties are provided for user authentication or identity token authentication.
|
|
||||||
Consult the documentation for the Docker registry being used to store builder or run images for further information on supported authentication methods.
|
|
||||||
|
|
||||||
The following table summarizes the available properties:
|
If the generated Docker image is to be published to a Docker image registry, the authentication credentials can be provided using `docker.publishRegistry` properties.
|
||||||
|
|
||||||
|
Properties are provided for user authentication or identity token authentication.
|
||||||
|
Consult the documentation for the Docker registry being used to store images for further information on supported authentication methods.
|
||||||
|
|
||||||
|
The following table summarizes the available properties for `docker.builderRegistry` and `docker.publishRegistry`:
|
||||||
|
|
||||||
|===
|
|===
|
||||||
| Property | Description
|
| Property | Description
|
||||||
|
|
@ -133,6 +136,11 @@ Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`.
|
||||||
|
|
|
|
||||||
| Enables verbose logging of builder operations.
|
| Enables verbose logging of builder operations.
|
||||||
| `false`
|
| `false`
|
||||||
|
|
||||||
|
| `publish`
|
||||||
|
| `--publishImage`
|
||||||
|
| Whether to publish the generated image to a Docker registry.
|
||||||
|
| `false`
|
||||||
|===
|
|===
|
||||||
|
|
||||||
NOTE: The plugin detects the target Java compatibility of the project using the JavaPlugin's `targetCompatibility` property.
|
NOTE: The plugin detects the target Java compatibility of the project using the JavaPlugin's `targetCompatibility` property.
|
||||||
|
|
@ -236,6 +244,29 @@ The image name can be specified on the command line as well, as shown in this ex
|
||||||
$ gradle bootBuildImage --imageName=example.com/library/my-app:v1
|
$ gradle bootBuildImage --imageName=example.com/library/my-app:v1
|
||||||
----
|
----
|
||||||
|
|
||||||
|
[[build-image-example-publish]]
|
||||||
|
==== Image Publishing
|
||||||
|
The generated image can be published to a Docker registry by enabling a `publish` option and configuring authentication for the registry using `docker.publishRegistry` properties.
|
||||||
|
|
||||||
|
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||||
|
.Groovy
|
||||||
|
----
|
||||||
|
include::../gradle/packaging/boot-build-image-publish.gradle[tags=publish]
|
||||||
|
----
|
||||||
|
|
||||||
|
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
|
||||||
|
.Kotlin
|
||||||
|
----
|
||||||
|
include::../gradle/packaging/boot-build-image-publish.gradle.kts[tags=publish]
|
||||||
|
----
|
||||||
|
|
||||||
|
The publish option can be specified on the command line as well, as shown in this example:
|
||||||
|
|
||||||
|
[indent=0]
|
||||||
|
----
|
||||||
|
$ gradle bootBuildImage --imageName=docker.example.com/library/my-app:v1 --publishImage
|
||||||
|
----
|
||||||
|
|
||||||
[[build-image-example-docker]]
|
[[build-image-example-docker]]
|
||||||
==== Docker Configuration
|
==== Docker Configuration
|
||||||
If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` properties as shown in the following example:
|
If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` properties as shown in the following example:
|
||||||
|
|
@ -252,7 +283,7 @@ include::../gradle/packaging/boot-build-image-docker-host.gradle[tags=docker-hos
|
||||||
include::../gradle/packaging/boot-build-image-docker-host.gradle.kts[tags=docker-host]
|
include::../gradle/packaging/boot-build-image-docker-host.gradle.kts[tags=docker-host]
|
||||||
----
|
----
|
||||||
|
|
||||||
If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided using `docker.registry` properties as shown in the following example:
|
If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided using `docker.buiderRegistry` properties as shown in the following example:
|
||||||
|
|
||||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||||
.Groovy
|
.Groovy
|
||||||
|
|
@ -266,7 +297,7 @@ include::../gradle/packaging/boot-build-image-docker-auth-user.gradle[tags=docke
|
||||||
include::../gradle/packaging/boot-build-image-docker-auth-user.gradle.kts[tags=docker-auth-user]
|
include::../gradle/packaging/boot-build-image-docker-auth-user.gradle.kts[tags=docker-auth-user]
|
||||||
----
|
----
|
||||||
|
|
||||||
If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided using `docker.registry` as shown in the following example:
|
If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided using `docker.builderRegistry` as shown in the following example:
|
||||||
|
|
||||||
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
|
||||||
.Groovy
|
.Groovy
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ bootJar {
|
||||||
// tag::docker-auth-token[]
|
// tag::docker-auth-token[]
|
||||||
bootBuildImage {
|
bootBuildImage {
|
||||||
docker {
|
docker {
|
||||||
registry {
|
builderRegistry {
|
||||||
token = "9cbaf023786cd7..."
|
token = "9cbaf023786cd7..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ tasks.getByName<BootJar>("bootJar") {
|
||||||
// tag::docker-auth-token[]
|
// tag::docker-auth-token[]
|
||||||
tasks.getByName<BootBuildImage>("bootBuildImage") {
|
tasks.getByName<BootBuildImage>("bootBuildImage") {
|
||||||
docker {
|
docker {
|
||||||
registry {
|
builderRegistry {
|
||||||
token = "9cbaf023786cd7..."
|
token = "9cbaf023786cd7..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ bootJar {
|
||||||
// tag::docker-auth-user[]
|
// tag::docker-auth-user[]
|
||||||
bootBuildImage {
|
bootBuildImage {
|
||||||
docker {
|
docker {
|
||||||
registry {
|
builderRegistry {
|
||||||
username = "user"
|
username = "user"
|
||||||
password = "secret"
|
password = "secret"
|
||||||
url = "https://docker.example.com/v1/"
|
url = "https://docker.example.com/v1/"
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ tasks.getByName<BootJar>("bootJar") {
|
||||||
// tag::docker-auth-user[]
|
// tag::docker-auth-user[]
|
||||||
tasks.getByName<BootBuildImage>("bootBuildImage") {
|
tasks.getByName<BootBuildImage>("bootBuildImage") {
|
||||||
docker {
|
docker {
|
||||||
registry {
|
builderRegistry {
|
||||||
username = "user"
|
username = "user"
|
||||||
password = "secret"
|
password = "secret"
|
||||||
url = "https://docker.example.com/v1/"
|
url = "https://docker.example.com/v1/"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'org.springframework.boot' version '{gradle-project-version}'
|
||||||
|
}
|
||||||
|
|
||||||
|
bootJar {
|
||||||
|
mainClassName 'com.example.ExampleApplication'
|
||||||
|
}
|
||||||
|
|
||||||
|
// tag::publish[]
|
||||||
|
bootBuildImage {
|
||||||
|
imageName = "docker.example.com/library/${project.name}"
|
||||||
|
publish = true
|
||||||
|
docker {
|
||||||
|
publishRegistry {
|
||||||
|
username = "user"
|
||||||
|
password = "secret"
|
||||||
|
url = "https://docker.example.com/v1/"
|
||||||
|
email = "user@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end::publish[]
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
import org.springframework.boot.gradle.tasks.bundling.BootJar
|
||||||
|
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
java
|
||||||
|
id("org.springframework.boot") version "{gradle-project-version}"
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.getByName<BootJar>("bootJar") {
|
||||||
|
mainClassName = "com.example.ExampleApplication"
|
||||||
|
}
|
||||||
|
|
||||||
|
// tag::publish[]
|
||||||
|
tasks.getByName<BootBuildImage>("bootBuildImage") {
|
||||||
|
imageName = "docker.example.com/library/${project.name}"
|
||||||
|
publish = true
|
||||||
|
docker {
|
||||||
|
publishRegistry {
|
||||||
|
username = "user"
|
||||||
|
password = "secret"
|
||||||
|
url = "https://docker.example.com/v1/"
|
||||||
|
email = "user@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end::publish[]
|
||||||
|
|
@ -23,6 +23,7 @@ import java.util.Map;
|
||||||
import groovy.lang.Closure;
|
import groovy.lang.Closure;
|
||||||
import org.gradle.api.Action;
|
import org.gradle.api.Action;
|
||||||
import org.gradle.api.DefaultTask;
|
import org.gradle.api.DefaultTask;
|
||||||
|
import org.gradle.api.GradleException;
|
||||||
import org.gradle.api.JavaVersion;
|
import org.gradle.api.JavaVersion;
|
||||||
import org.gradle.api.Project;
|
import org.gradle.api.Project;
|
||||||
import org.gradle.api.Task;
|
import org.gradle.api.Task;
|
||||||
|
|
@ -76,6 +77,8 @@ public class BootBuildImage extends DefaultTask {
|
||||||
|
|
||||||
private PullPolicy pullPolicy;
|
private PullPolicy pullPolicy;
|
||||||
|
|
||||||
|
private boolean publish;
|
||||||
|
|
||||||
private DockerSpec docker = new DockerSpec();
|
private DockerSpec docker = new DockerSpec();
|
||||||
|
|
||||||
public BootBuildImage() {
|
public BootBuildImage() {
|
||||||
|
|
@ -252,6 +255,24 @@ public class BootBuildImage extends DefaultTask {
|
||||||
this.pullPolicy = pullPolicy;
|
this.pullPolicy = pullPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the built image should be pushed to a registry.
|
||||||
|
* @return whether the built image should be pushed
|
||||||
|
*/
|
||||||
|
@Input
|
||||||
|
public boolean isPublish() {
|
||||||
|
return this.publish;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets whether the built image should be pushed to a registry.
|
||||||
|
* @param publish {@code true} the push the built image to a registry. {@code false}.
|
||||||
|
*/
|
||||||
|
@Option(option = "publishImage", description = "Publish the built image to a registry")
|
||||||
|
public void setPublish(boolean publish) {
|
||||||
|
this.publish = publish;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the Docker configuration the builder will use.
|
* Returns the Docker configuration the builder will use.
|
||||||
* @return docker configuration.
|
* @return docker configuration.
|
||||||
|
|
@ -312,6 +333,7 @@ public class BootBuildImage extends DefaultTask {
|
||||||
request = request.withCleanCache(this.cleanCache);
|
request = request.withCleanCache(this.cleanCache);
|
||||||
request = request.withVerboseLogging(this.verboseLogging);
|
request = request.withVerboseLogging(this.verboseLogging);
|
||||||
request = customizePullPolicy(request);
|
request = customizePullPolicy(request);
|
||||||
|
request = customizePublish(request);
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,6 +376,16 @@ public class BootBuildImage extends DefaultTask {
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BuildRequest customizePublish(BuildRequest request) {
|
||||||
|
boolean publishRegistryAuthNotConfigured = this.docker == null || this.docker.getPublishRegistry() == null
|
||||||
|
|| this.docker.getPublishRegistry().hasEmptyAuth();
|
||||||
|
if (this.publish && publishRegistryAuthNotConfigured) {
|
||||||
|
throw new GradleException("Publishing an image requires docker.publishRegistry to be configured");
|
||||||
|
}
|
||||||
|
request = request.withPublish(this.publish);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
private String translateTargetJavaVersion() {
|
private String translateTargetJavaVersion() {
|
||||||
return this.targetJavaVersion.get().getMajorVersion() + ".*";
|
return this.targetJavaVersion.get().getMajorVersion() + ".*";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,14 +41,18 @@ public class DockerSpec {
|
||||||
|
|
||||||
private String certPath;
|
private String certPath;
|
||||||
|
|
||||||
private final DockerRegistrySpec registry;
|
private final DockerRegistrySpec builderRegistry;
|
||||||
|
|
||||||
|
private final DockerRegistrySpec publishRegistry;
|
||||||
|
|
||||||
public DockerSpec() {
|
public DockerSpec() {
|
||||||
this.registry = new DockerRegistrySpec();
|
this.builderRegistry = new DockerRegistrySpec();
|
||||||
|
this.publishRegistry = new DockerRegistrySpec();
|
||||||
}
|
}
|
||||||
|
|
||||||
DockerSpec(DockerRegistrySpec registry) {
|
DockerSpec(DockerRegistrySpec builderRegistry, DockerRegistrySpec publishRegistry) {
|
||||||
this.registry = registry;
|
this.builderRegistry = builderRegistry;
|
||||||
|
this.publishRegistry = publishRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input
|
@Input
|
||||||
|
|
@ -82,28 +86,59 @@ public class DockerSpec {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the {@link DockerRegistrySpec} that configures registry authentication.
|
* Returns the {@link DockerRegistrySpec} that configures authentication to the
|
||||||
|
* builder registry.
|
||||||
* @return the registry spec
|
* @return the registry spec
|
||||||
*/
|
*/
|
||||||
@Nested
|
@Nested
|
||||||
public DockerRegistrySpec getRegistry() {
|
public DockerRegistrySpec getBuilderRegistry() {
|
||||||
return this.registry;
|
return this.builderRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Customizes the {@link DockerRegistrySpec} that configures registry authentication.
|
* Customizes the {@link DockerRegistrySpec} that configures authentication to the
|
||||||
|
* builder registry.
|
||||||
* @param action the action to apply
|
* @param action the action to apply
|
||||||
*/
|
*/
|
||||||
public void registry(Action<DockerRegistrySpec> action) {
|
public void builderRegistry(Action<DockerRegistrySpec> action) {
|
||||||
action.execute(this.registry);
|
action.execute(this.builderRegistry);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Customizes the {@link DockerRegistrySpec} that configures registry authentication.
|
* Customizes the {@link DockerRegistrySpec} that configures authentication to the
|
||||||
|
* builder registry.
|
||||||
* @param closure the closure to apply
|
* @param closure the closure to apply
|
||||||
*/
|
*/
|
||||||
public void registry(Closure<?> closure) {
|
public void builderRegistry(Closure<?> closure) {
|
||||||
registry(ConfigureUtil.configureUsing(closure));
|
builderRegistry(ConfigureUtil.configureUsing(closure));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the {@link DockerRegistrySpec} that configures authentication to the
|
||||||
|
* publishing registry.
|
||||||
|
* @return the registry spec
|
||||||
|
*/
|
||||||
|
@Nested
|
||||||
|
public DockerRegistrySpec getPublishRegistry() {
|
||||||
|
return this.publishRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customizes the {@link DockerRegistrySpec} that configures authentication to the
|
||||||
|
* publishing registry.
|
||||||
|
* @param action the action to apply
|
||||||
|
*/
|
||||||
|
public void publishRegistry(Action<DockerRegistrySpec> action) {
|
||||||
|
action.execute(this.publishRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Customizes the {@link DockerRegistrySpec} that configures authentication to the
|
||||||
|
* publishing registry.
|
||||||
|
* @param closure the closure to apply
|
||||||
|
*/
|
||||||
|
public void publishRegistry(Closure<?> closure) {
|
||||||
|
publishRegistry(ConfigureUtil.configureUsing(closure));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -115,7 +150,8 @@ public class DockerSpec {
|
||||||
DockerConfiguration asDockerConfiguration() {
|
DockerConfiguration asDockerConfiguration() {
|
||||||
DockerConfiguration dockerConfiguration = new DockerConfiguration();
|
DockerConfiguration dockerConfiguration = new DockerConfiguration();
|
||||||
dockerConfiguration = customizeHost(dockerConfiguration);
|
dockerConfiguration = customizeHost(dockerConfiguration);
|
||||||
dockerConfiguration = customizeAuthentication(dockerConfiguration);
|
dockerConfiguration = customizeBuilderAuthentication(dockerConfiguration);
|
||||||
|
dockerConfiguration = customizePublishAuthentication(dockerConfiguration);
|
||||||
return dockerConfiguration;
|
return dockerConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,19 +162,34 @@ public class DockerSpec {
|
||||||
return dockerConfiguration;
|
return dockerConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DockerConfiguration customizeAuthentication(DockerConfiguration dockerConfiguration) {
|
private DockerConfiguration customizeBuilderAuthentication(DockerConfiguration dockerConfiguration) {
|
||||||
if (this.registry == null || this.registry.hasEmptyAuth()) {
|
if (this.builderRegistry == null || this.builderRegistry.hasEmptyAuth()) {
|
||||||
return dockerConfiguration;
|
return dockerConfiguration;
|
||||||
}
|
}
|
||||||
if (this.registry.hasTokenAuth() && !this.registry.hasUserAuth()) {
|
if (this.builderRegistry.hasTokenAuth() && !this.builderRegistry.hasUserAuth()) {
|
||||||
return dockerConfiguration.withRegistryTokenAuthentication(this.registry.getToken());
|
return dockerConfiguration.withBuilderRegistryTokenAuthentication(this.builderRegistry.getToken());
|
||||||
}
|
}
|
||||||
if (this.registry.hasUserAuth() && !this.registry.hasTokenAuth()) {
|
if (this.builderRegistry.hasUserAuth() && !this.builderRegistry.hasTokenAuth()) {
|
||||||
return dockerConfiguration.withRegistryUserAuthentication(this.registry.getUsername(),
|
return dockerConfiguration.withBuilderRegistryUserAuthentication(this.builderRegistry.getUsername(),
|
||||||
this.registry.getPassword(), this.registry.getUrl(), this.registry.getEmail());
|
this.builderRegistry.getPassword(), this.builderRegistry.getUrl(), this.builderRegistry.getEmail());
|
||||||
}
|
}
|
||||||
throw new GradleException(
|
throw new GradleException(
|
||||||
"Invalid Docker registry configuration, either token or username/password must be provided");
|
"Invalid Docker builder registry configuration, either token or username/password must be provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
private DockerConfiguration customizePublishAuthentication(DockerConfiguration dockerConfiguration) {
|
||||||
|
if (this.publishRegistry == null || this.publishRegistry.hasEmptyAuth()) {
|
||||||
|
return dockerConfiguration;
|
||||||
|
}
|
||||||
|
if (this.publishRegistry.hasTokenAuth() && !this.publishRegistry.hasUserAuth()) {
|
||||||
|
return dockerConfiguration.withPublishRegistryTokenAuthentication(this.publishRegistry.getToken());
|
||||||
|
}
|
||||||
|
if (this.publishRegistry.hasUserAuth() && !this.publishRegistry.hasTokenAuth()) {
|
||||||
|
return dockerConfiguration.withPublishRegistryUserAuthentication(this.publishRegistry.getUsername(),
|
||||||
|
this.publishRegistry.getPassword(), this.publishRegistry.getUrl(), this.publishRegistry.getEmail());
|
||||||
|
}
|
||||||
|
throw new GradleException(
|
||||||
|
"Invalid Docker publish registry configuration, either token or username/password must be provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -156,6 +207,20 @@ public class DockerSpec {
|
||||||
|
|
||||||
private String token;
|
private String token;
|
||||||
|
|
||||||
|
public DockerRegistrySpec() {
|
||||||
|
}
|
||||||
|
|
||||||
|
DockerRegistrySpec(String username, String password, String url, String email) {
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.url = url;
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
DockerRegistrySpec(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the username to use when authenticating to the Docker registry.
|
* Returns the username to use when authenticating to the Docker registry.
|
||||||
* @return the registry username
|
* @return the registry username
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,15 @@ class BootBuildImageIntegrationTests {
|
||||||
.containsPattern("example/Invalid-Image-Name");
|
.containsPattern("example/Invalid-Image-Name");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestTemplate
|
||||||
|
void failsWithPublishMissingPublishRegistry() {
|
||||||
|
writeMainClass();
|
||||||
|
writeLongNameResource();
|
||||||
|
BuildResult result = this.gradleBuild.buildAndFail("bootBuildImage", "--publishImage");
|
||||||
|
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.FAILED);
|
||||||
|
assertThat(result.getOutput()).contains("requires docker.publishRegistry");
|
||||||
|
}
|
||||||
|
|
||||||
private void writeMainClass() {
|
private void writeMainClass() {
|
||||||
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
|
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
|
||||||
examplePackage.mkdirs();
|
examplePackage.mkdirs();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2020 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.gradle.tasks.bundling;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import org.gradle.testkit.runner.BuildResult;
|
||||||
|
import org.gradle.testkit.runner.TaskOutcome;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.TestTemplate;
|
||||||
|
import org.testcontainers.containers.GenericContainer;
|
||||||
|
import org.testcontainers.junit.jupiter.Container;
|
||||||
|
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||||
|
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.DockerApi;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.UpdateListener;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.type.Image;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||||
|
import org.springframework.boot.gradle.junit.GradleCompatibility;
|
||||||
|
import org.springframework.boot.gradle.testkit.GradleBuild;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for {@link BootBuildImage} tasks requiring a Docker image registry.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
|
*/
|
||||||
|
@GradleCompatibility
|
||||||
|
@Testcontainers(disabledWithoutDocker = true)
|
||||||
|
public class BootBuildImageRegistryIntegrationTests {
|
||||||
|
|
||||||
|
@Container
|
||||||
|
static final RegistryContainer registry = new RegistryContainer().withStartupAttempts(5)
|
||||||
|
.withStartupTimeout(Duration.ofMinutes(3));
|
||||||
|
|
||||||
|
String registryAddress;
|
||||||
|
|
||||||
|
GradleBuild gradleBuild;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
assertThat(registry.isRunning());
|
||||||
|
this.registryAddress = registry.getHost() + ":" + registry.getFirstMappedPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestTemplate
|
||||||
|
void buildsImageAndPublishesToRegistry() throws IOException, InterruptedException {
|
||||||
|
writeMainClass();
|
||||||
|
String repoName = "test-image";
|
||||||
|
String imageName = this.registryAddress + "/" + repoName;
|
||||||
|
BuildResult result = this.gradleBuild.build("bootBuildImage", "--imageName=" + imageName);
|
||||||
|
assertThat(result.task(":bootBuildImage").getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
|
||||||
|
assertThat(result.getOutput()).contains("Building image").contains("Successfully built image")
|
||||||
|
.contains("Pushing image '" + imageName + ":latest" + "'")
|
||||||
|
.contains("Pushed image '" + imageName + ":latest" + "'");
|
||||||
|
ImageReference imageReference = ImageReference.of(imageName);
|
||||||
|
Image pulledImage = new DockerApi().image().pull(imageReference, UpdateListener.none());
|
||||||
|
assertThat(pulledImage).isNotNull();
|
||||||
|
new DockerApi().image().remove(imageReference, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeMainClass() {
|
||||||
|
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
|
||||||
|
examplePackage.mkdirs();
|
||||||
|
File main = new File(examplePackage, "Main.java");
|
||||||
|
try (PrintWriter writer = new PrintWriter(new FileWriter(main))) {
|
||||||
|
writer.println("package example;");
|
||||||
|
writer.println();
|
||||||
|
writer.println("import java.io.IOException;");
|
||||||
|
writer.println();
|
||||||
|
writer.println("public class Main {");
|
||||||
|
writer.println();
|
||||||
|
writer.println(" public static void main(String[] args) {");
|
||||||
|
writer.println(" }");
|
||||||
|
writer.println();
|
||||||
|
writer.println("}");
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RegistryContainer extends GenericContainer<RegistryContainer> {
|
||||||
|
|
||||||
|
RegistryContainer() {
|
||||||
|
super("registry:2.7.1");
|
||||||
|
addExposedPorts(5000);
|
||||||
|
addEnv("SERVER_NAME", "localhost");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ import java.io.File;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.gradle.api.GradleException;
|
||||||
import org.gradle.api.JavaVersion;
|
import org.gradle.api.JavaVersion;
|
||||||
import org.gradle.api.Project;
|
import org.gradle.api.Project;
|
||||||
import org.gradle.testfixtures.ProjectBuilder;
|
import org.gradle.testfixtures.ProjectBuilder;
|
||||||
|
|
@ -30,6 +31,7 @@ import org.springframework.boot.buildpack.platform.build.BuildRequest;
|
||||||
import org.springframework.boot.buildpack.platform.build.PullPolicy;
|
import org.springframework.boot.buildpack.platform.build.PullPolicy;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link BootBuildImage}.
|
* Tests for {@link BootBuildImage}.
|
||||||
|
|
@ -174,6 +176,18 @@ class BootBuildImageTests {
|
||||||
assertThat(this.buildImage.createRequest().isCleanCache()).isTrue();
|
assertThat(this.buildImage.createRequest().isCleanCache()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenUsingDefaultConfigurationThenRequestHasPublishDisabled() {
|
||||||
|
assertThat(this.buildImage.createRequest().isPublish()).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void whenPublishIsEnabledWithoutPublishRegistryThenExceptionIsThrown() {
|
||||||
|
this.buildImage.setPublish(true);
|
||||||
|
assertThatExceptionOfType(GradleException.class).isThrownBy(this.buildImage::createRequest)
|
||||||
|
.withMessageContaining("Publishing an image requires docker.publishRegistry to be configured");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void whenNoBuilderIsConfiguredThenRequestHasDefaultBuilder() {
|
void whenNoBuilderIsConfiguredThenRequestHasDefaultBuilder() {
|
||||||
assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("paketo-buildpacks/builder");
|
assertThat(this.buildImage.createRequest().getBuilder().getName()).isEqualTo("paketo-buildpacks/builder");
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost;
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost;
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
|
|
||||||
import org.springframework.util.Base64Utils;
|
import org.springframework.util.Base64Utils;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
@ -39,7 +38,8 @@ public class DockerSpecTests {
|
||||||
void asDockerConfigurationWithDefaults() {
|
void asDockerConfigurationWithDefaults() {
|
||||||
DockerSpec dockerSpec = new DockerSpec();
|
DockerSpec dockerSpec = new DockerSpec();
|
||||||
assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull();
|
assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull();
|
||||||
assertThat(dockerSpec.asDockerConfiguration().getRegistryAuthentication()).isNull();
|
assertThat(dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
|
||||||
|
assertThat(dockerSpec.asDockerConfiguration().getPublishRegistryAuthentication()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -53,7 +53,8 @@ public class DockerSpecTests {
|
||||||
assertThat(host.getAddress()).isEqualTo("docker.example.com");
|
assertThat(host.getAddress()).isEqualTo("docker.example.com");
|
||||||
assertThat(host.isSecure()).isEqualTo(true);
|
assertThat(host.isSecure()).isEqualTo(true);
|
||||||
assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert");
|
assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert");
|
||||||
assertThat(dockerSpec.asDockerConfiguration().getRegistryAuthentication()).isNull();
|
assertThat(dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
|
||||||
|
assertThat(dockerSpec.asDockerConfiguration().getPublishRegistryAuthentication()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -65,59 +66,71 @@ public class DockerSpecTests {
|
||||||
assertThat(host.getAddress()).isEqualTo("docker.example.com");
|
assertThat(host.getAddress()).isEqualTo("docker.example.com");
|
||||||
assertThat(host.isSecure()).isEqualTo(false);
|
assertThat(host.isSecure()).isEqualTo(false);
|
||||||
assertThat(host.getCertificatePath()).isNull();
|
assertThat(host.getCertificatePath()).isNull();
|
||||||
assertThat(dockerSpec.asDockerConfiguration().getRegistryAuthentication()).isNull();
|
assertThat(dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
|
||||||
|
assertThat(dockerSpec.asDockerConfiguration().getPublishRegistryAuthentication()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void asDockerConfigurationWithUserAuth() {
|
void asDockerConfigurationWithUserAuth() {
|
||||||
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
|
DockerSpec dockerSpec = new DockerSpec(
|
||||||
dockerRegistry.setUsername("user");
|
new DockerSpec.DockerRegistrySpec("user1", "secret1", "https://docker1.example.com",
|
||||||
dockerRegistry.setPassword("secret");
|
"docker1@example.com"),
|
||||||
dockerRegistry.setUrl("https://docker.example.com");
|
new DockerSpec.DockerRegistrySpec("user2", "secret2", "https://docker2.example.com",
|
||||||
dockerRegistry.setEmail("docker@example.com");
|
"docker2@example.com"));
|
||||||
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
|
|
||||||
DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration();
|
DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration();
|
||||||
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
|
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
|
||||||
assertThat(registryAuthentication).isNotNull();
|
.contains("\"username\" : \"user1\"").contains("\"password\" : \"secret1\"")
|
||||||
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
|
.contains("\"email\" : \"docker1@example.com\"")
|
||||||
.contains("\"username\" : \"user\"").contains("\"password\" : \"secret\"")
|
.contains("\"serveraddress\" : \"https://docker1.example.com\"");
|
||||||
.contains("\"email\" : \"docker@example.com\"")
|
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
|
||||||
.contains("\"serveraddress\" : \"https://docker.example.com\"");
|
.contains("\"username\" : \"user2\"").contains("\"password\" : \"secret2\"")
|
||||||
|
.contains("\"email\" : \"docker2@example.com\"")
|
||||||
|
.contains("\"serveraddress\" : \"https://docker2.example.com\"");
|
||||||
assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull();
|
assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void asDockerConfigurationWithIncompleteUserAuthFails() {
|
void asDockerConfigurationWithIncompleteBuilderUserAuthFails() {
|
||||||
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
|
DockerSpec.DockerRegistrySpec builderRegistry = new DockerSpec.DockerRegistrySpec("user", null,
|
||||||
dockerRegistry.setUsername("user");
|
"https://docker.example.com", "docker@example.com");
|
||||||
dockerRegistry.setUrl("https://docker.example.com");
|
DockerSpec dockerSpec = new DockerSpec(builderRegistry, null);
|
||||||
dockerRegistry.setEmail("docker@example.com");
|
|
||||||
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
|
|
||||||
assertThatExceptionOfType(GradleException.class).isThrownBy(dockerSpec::asDockerConfiguration)
|
assertThatExceptionOfType(GradleException.class).isThrownBy(dockerSpec::asDockerConfiguration)
|
||||||
.withMessageContaining("Invalid Docker registry configuration");
|
.withMessageContaining("Invalid Docker builder registry configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void asDockerConfigurationWithIncompletePublishUserAuthFails() {
|
||||||
|
DockerSpec.DockerRegistrySpec publishRegistry = new DockerSpec.DockerRegistrySpec("user2", null,
|
||||||
|
"https://docker2.example.com", "docker2@example.com");
|
||||||
|
DockerSpec dockerSpec = new DockerSpec(null, publishRegistry);
|
||||||
|
assertThatExceptionOfType(GradleException.class).isThrownBy(dockerSpec::asDockerConfiguration)
|
||||||
|
.withMessageContaining("Invalid Docker publish registry configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void asDockerConfigurationWithTokenAuth() {
|
void asDockerConfigurationWithTokenAuth() {
|
||||||
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
|
DockerSpec dockerSpec = new DockerSpec(new DockerSpec.DockerRegistrySpec("token1"),
|
||||||
dockerRegistry.setToken("token");
|
new DockerSpec.DockerRegistrySpec("token2"));
|
||||||
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
|
|
||||||
DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration();
|
DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration();
|
||||||
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
|
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
|
||||||
assertThat(registryAuthentication).isNotNull();
|
.contains("\"identitytoken\" : \"token1\"");
|
||||||
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
|
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
|
||||||
.contains("\"identitytoken\" : \"token\"");
|
.contains("\"identitytoken\" : \"token2\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void asDockerConfigurationWithUserAndTokenAuthFails() {
|
void asDockerConfigurationWithUserAndTokenAuthFails() {
|
||||||
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
|
DockerSpec.DockerRegistrySpec builderRegistry = new DockerSpec.DockerRegistrySpec();
|
||||||
dockerRegistry.setUsername("user");
|
builderRegistry.setUsername("user");
|
||||||
dockerRegistry.setPassword("secret");
|
builderRegistry.setPassword("secret");
|
||||||
dockerRegistry.setToken("token");
|
builderRegistry.setToken("token");
|
||||||
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
|
DockerSpec dockerSpec = new DockerSpec(builderRegistry, null);
|
||||||
assertThatExceptionOfType(GradleException.class).isThrownBy(dockerSpec::asDockerConfiguration)
|
assertThatExceptionOfType(GradleException.class).isThrownBy(dockerSpec::asDockerConfiguration)
|
||||||
.withMessageContaining("Invalid Docker registry configuration");
|
.withMessageContaining("Invalid Docker builder registry configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
String decoded(String value) {
|
||||||
|
return new String(Base64Utils.decodeFromString(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
plugins {
|
||||||
|
id 'java'
|
||||||
|
id 'org.springframework.boot' version '{version}'
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceCompatibility = '1.8'
|
||||||
|
targetCompatibility = '1.8'
|
||||||
|
|
||||||
|
bootBuildImage {
|
||||||
|
publish = true
|
||||||
|
docker {
|
||||||
|
publishRegistry {
|
||||||
|
username = "user"
|
||||||
|
password = "secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ dependencies {
|
||||||
intTestImplementation("org.assertj:assertj-core")
|
intTestImplementation("org.assertj:assertj-core")
|
||||||
intTestImplementation("org.junit.jupiter:junit-jupiter")
|
intTestImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
intTestImplementation("org.testcontainers:testcontainers")
|
intTestImplementation("org.testcontainers:testcontainers")
|
||||||
|
intTestImplementation("org.testcontainers:junit-jupiter")
|
||||||
|
|
||||||
optional("org.apache.maven.plugins:maven-shade-plugin")
|
optional("org.apache.maven.plugins:maven-shade-plugin")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,14 @@ For more details, see also <<build-image-example-docker,examples>>.
|
||||||
|
|
||||||
[[build-image-docker-registry]]
|
[[build-image-docker-registry]]
|
||||||
=== Docker Registry
|
=== Docker Registry
|
||||||
If the Docker images specified by the `builder` or `runImage` parameters are stored in a private Docker image registry that requires authentication, the authentication credentials can be provided using `docker.registry` parameters.
|
If the Docker images specified by the `builder` or `runImage` parameters are stored in a private Docker image registry that requires authentication, the authentication credentials can be provided using `docker.builderRegistry` parameters.
|
||||||
Parameters are provided for user authentication or identity token authentication.
|
|
||||||
Consult the documentation for the Docker registry being used to store builder or run images for further information on supported authentication methods.
|
|
||||||
|
|
||||||
The following table summarizes the available parameters:
|
If the generated Docker image is to be published to a Docker image registry, the authentication credentials can be provided using `docker.publishRegistry` parameters.
|
||||||
|
|
||||||
|
Parameters are provided for user authentication or identity token authentication.
|
||||||
|
Consult the documentation for the Docker registry being used to store images for further information on supported authentication methods.
|
||||||
|
|
||||||
|
The following table summarizes the available parameters for `docker.builderRegistry` and `docker.publishRegistry`:
|
||||||
|
|
||||||
|===
|
|===
|
||||||
| Parameter | Description
|
| Parameter | Description
|
||||||
|
|
@ -156,6 +159,11 @@ Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`.
|
||||||
| Enables verbose logging of builder operations.
|
| Enables verbose logging of builder operations.
|
||||||
|
|
|
|
||||||
| `false`
|
| `false`
|
||||||
|
|
||||||
|
| `publish`
|
||||||
|
| Whether to publish the generated image to a Docker registry.
|
||||||
|
| `spring-boot.build-image.publish`
|
||||||
|
| `false`
|
||||||
|===
|
|===
|
||||||
|
|
||||||
NOTE: The plugin detects the target Java compatibility of the project using the compiler's plugin configuration or the `maven.compiler.target` property.
|
NOTE: The plugin detects the target Java compatibility of the project using the compiler's plugin configuration or the `maven.compiler.target` property.
|
||||||
|
|
@ -303,6 +311,48 @@ The image name can be specified on the command line as well, as shown in this ex
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[build-image-example-publish]]
|
||||||
|
==== Image Publishing
|
||||||
|
The generated image can be published to a Docker registry by enabling a `publish` option and configuring authentication for the registry using `docker.publishRegistry` parameters.
|
||||||
|
|
||||||
|
[source,xml,indent=0,subs="verbatim,attributes"]
|
||||||
|
----
|
||||||
|
<project>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
<version>{gradle-project-version}</version>
|
||||||
|
<configuration>
|
||||||
|
<image>
|
||||||
|
<name>docker.example.com/library/${project.artifactId}</name>
|
||||||
|
<publish>true</publish>
|
||||||
|
</image>
|
||||||
|
<docker>
|
||||||
|
<publishRegistry>
|
||||||
|
<username>user</username>
|
||||||
|
<password>secret</password>
|
||||||
|
<url>https://docker.example.com/v1/</url>
|
||||||
|
<email>user@example.com</email>
|
||||||
|
</builderpublish>
|
||||||
|
</docker>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
|
----
|
||||||
|
|
||||||
|
The `publish` option can be specified on the command line as well, as shown in this example:
|
||||||
|
|
||||||
|
[indent=0]
|
||||||
|
----
|
||||||
|
$ mvn spring-boot:build-image -Dspring-boot.build-image.imageName=docker.example.com/library/my-app:v1 -Dspring-boot.build-image.publish=true
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[[build-image-example-docker]]
|
[[build-image-example-docker]]
|
||||||
==== Docker Configuration
|
==== Docker Configuration
|
||||||
If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` parameters as shown in the following example:
|
If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` parameters as shown in the following example:
|
||||||
|
|
@ -329,7 +379,7 @@ If you need the plugin to communicate with the Docker daemon using a remote conn
|
||||||
</project>
|
</project>
|
||||||
----
|
----
|
||||||
|
|
||||||
If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided using `docker.registry` parameters as shown in the following example:
|
If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided using `docker.builderRegistry` parameters as shown in the following example:
|
||||||
|
|
||||||
[source,xml,indent=0,subs="verbatim,attributes"]
|
[source,xml,indent=0,subs="verbatim,attributes"]
|
||||||
----
|
----
|
||||||
|
|
@ -342,12 +392,12 @@ If the builder or run image are stored in a private Docker registry that support
|
||||||
<version>{gradle-project-version}</version>
|
<version>{gradle-project-version}</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<docker>
|
<docker>
|
||||||
<registry>
|
<builderRegistry>
|
||||||
<username>user</username>
|
<username>user</username>
|
||||||
<password>secret</password>
|
<password>secret</password>
|
||||||
<url>https://docker.example.com/v1/</url>
|
<url>https://docker.example.com/v1/</url>
|
||||||
<email>user@example.com</email>
|
<email>user@example.com</email>
|
||||||
</registry>
|
</builderRegistry>
|
||||||
</docker>
|
</docker>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
@ -356,7 +406,7 @@ If the builder or run image are stored in a private Docker registry that support
|
||||||
</project>
|
</project>
|
||||||
----
|
----
|
||||||
|
|
||||||
If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided using `docker.registry` parameters as shown in the following example:
|
If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided using `docker.builderRegistry` parameters as shown in the following example:
|
||||||
|
|
||||||
[source,xml,indent=0,subs="verbatim,attributes"]
|
[source,xml,indent=0,subs="verbatim,attributes"]
|
||||||
----
|
----
|
||||||
|
|
@ -369,9 +419,9 @@ If the builder or run image is stored in a private Docker registry that supports
|
||||||
<version>{gradle-project-version}</version>
|
<version>{gradle-project-version}</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<docker>
|
<docker>
|
||||||
<registry>
|
<builderRegistry>
|
||||||
<token>9cbaf023786cd7...</token>
|
<token>9cbaf023786cd7...</token>
|
||||||
</registry>
|
</builderRegistry>
|
||||||
</docker>
|
</docker>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2020 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.boot.maven;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
import com.github.dockerjava.api.DockerClient;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.TestTemplate;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.testcontainers.containers.GenericContainer;
|
||||||
|
import org.testcontainers.junit.jupiter.Container;
|
||||||
|
import org.testcontainers.junit.jupiter.Testcontainers;
|
||||||
|
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.DockerApi;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.UpdateListener;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.type.Image;
|
||||||
|
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration tests for the Maven plugin's image support using a Docker image registry.
|
||||||
|
*
|
||||||
|
* @author Scott Frederick
|
||||||
|
*/
|
||||||
|
@ExtendWith(MavenBuildExtension.class)
|
||||||
|
@Testcontainers(disabledWithoutDocker = true)
|
||||||
|
public class BuildImageRegistryIntegrationTests extends AbstractArchiveIntegrationTests {
|
||||||
|
|
||||||
|
@Container
|
||||||
|
static final RegistryContainer registry = new RegistryContainer().withStartupAttempts(5)
|
||||||
|
.withStartupTimeout(Duration.ofMinutes(3));
|
||||||
|
|
||||||
|
DockerClient dockerClient;
|
||||||
|
|
||||||
|
String registryAddress;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
assertThat(registry.isRunning());
|
||||||
|
this.dockerClient = registry.getDockerClient();
|
||||||
|
this.registryAddress = registry.getHost() + ":" + registry.getFirstMappedPort();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TestTemplate
|
||||||
|
void whenBuildImageIsInvokedWithPublish(MavenBuild mavenBuild) {
|
||||||
|
String repoName = "test-image";
|
||||||
|
String imageName = this.registryAddress + "/" + repoName;
|
||||||
|
mavenBuild.project("build-image-publish").goals("package")
|
||||||
|
.systemProperty("spring-boot.build-image.imageName", imageName).execute((project) -> {
|
||||||
|
assertThat(buildLog(project)).contains("Building image").contains("Successfully built image")
|
||||||
|
.contains("Pushing image '" + imageName + ":latest" + "'")
|
||||||
|
.contains("Pushed image '" + imageName + ":latest" + "'");
|
||||||
|
ImageReference imageReference = ImageReference.of(imageName);
|
||||||
|
DockerApi.ImageApi imageApi = new DockerApi().image();
|
||||||
|
Image pulledImage = imageApi.pull(imageReference, UpdateListener.none());
|
||||||
|
assertThat(pulledImage).isNotNull();
|
||||||
|
imageApi.remove(imageReference, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class RegistryContainer extends GenericContainer<RegistryContainer> {
|
||||||
|
|
||||||
|
RegistryContainer() {
|
||||||
|
super("registry:2.7.1");
|
||||||
|
addExposedPorts(5000);
|
||||||
|
addEnv("SERVER_NAME", "localhost");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -148,6 +148,12 @@ public class BuildImageTests extends AbstractArchiveIntegrationTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TestTemplate
|
||||||
|
void failsWhenPublishWithoutPublishRegistryConfigured(MavenBuild mavenBuild) {
|
||||||
|
mavenBuild.project("build-image").goals("package").systemProperty("spring-boot.build-image.publish", "true")
|
||||||
|
.executeAndFail((project) -> assertThat(buildLog(project)).contains("requires docker.publishRegistry"));
|
||||||
|
}
|
||||||
|
|
||||||
@TestTemplate
|
@TestTemplate
|
||||||
void failsWhenBuilderFails(MavenBuild mavenBuild) {
|
void failsWhenBuilderFails(MavenBuild mavenBuild) {
|
||||||
mavenBuild.project("build-image-builder-error").goals("package")
|
mavenBuild.project("build-image-builder-error").goals("package")
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<?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</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>
|
||||||
|
<publish>true</publish>
|
||||||
|
</image>
|
||||||
|
<docker>
|
||||||
|
<publishRegistry>
|
||||||
|
<username>user</username>
|
||||||
|
<password>secret</password>
|
||||||
|
</publishRegistry>
|
||||||
|
</docker>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2012-2020 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.test;
|
||||||
|
|
||||||
|
public class SampleApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
System.out.println("Launched");
|
||||||
|
synchronized(args) {
|
||||||
|
args.wait(); // Prevent exit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -100,8 +100,8 @@ public class BuildImageMojo extends AbstractPackagerMojo {
|
||||||
private String classifier;
|
private String classifier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image configuration, with `builder`, `runImage`, `name`, `env`, `cleanCache` and
|
* Image configuration, with `builder`, `runImage`, `name`, `env`, `cleanCache`,
|
||||||
* `verboseLogging` options.
|
* `verboseLogging`, and `publish` options.
|
||||||
* @since 2.3.0
|
* @since 2.3.0
|
||||||
*/
|
*/
|
||||||
@Parameter
|
@Parameter
|
||||||
|
|
@ -136,6 +136,12 @@ public class BuildImageMojo extends AbstractPackagerMojo {
|
||||||
@Parameter(property = "spring-boot.build-image.pullPolicy", readonly = true)
|
@Parameter(property = "spring-boot.build-image.pullPolicy", readonly = true)
|
||||||
PullPolicy pullPolicy;
|
PullPolicy pullPolicy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for {@link Image#publish} to support configuration via command-line property.
|
||||||
|
*/
|
||||||
|
@Parameter(property = "spring-boot.build-image.publish", readonly = true)
|
||||||
|
Boolean publish;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Docker configuration options.
|
* Docker configuration options.
|
||||||
* @since 2.4.0
|
* @since 2.4.0
|
||||||
|
|
@ -170,7 +176,7 @@ public class BuildImageMojo extends AbstractPackagerMojo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private BuildRequest getBuildRequest(Libraries libraries) {
|
private BuildRequest getBuildRequest(Libraries libraries) throws MojoExecutionException {
|
||||||
Function<Owner, TarArchive> content = (owner) -> getApplicationContent(owner, libraries);
|
Function<Owner, TarArchive> content = (owner) -> getApplicationContent(owner, libraries);
|
||||||
Image image = (this.image != null) ? this.image : new Image();
|
Image image = (this.image != null) ? this.image : new Image();
|
||||||
if (image.name == null && this.imageName != null) {
|
if (image.name == null && this.imageName != null) {
|
||||||
|
|
@ -185,9 +191,20 @@ public class BuildImageMojo extends AbstractPackagerMojo {
|
||||||
if (image.pullPolicy == null && this.pullPolicy != null) {
|
if (image.pullPolicy == null && this.pullPolicy != null) {
|
||||||
image.setPullPolicy(this.pullPolicy);
|
image.setPullPolicy(this.pullPolicy);
|
||||||
}
|
}
|
||||||
|
if (image.publish == null && this.publish != null) {
|
||||||
|
image.setPublish(this.publish);
|
||||||
|
}
|
||||||
|
if (image.publish != null && image.publish && publishRegistryNotConfigured()) {
|
||||||
|
throw new MojoExecutionException("Publishing an image requires docker.publishRegistry to be configured");
|
||||||
|
}
|
||||||
return customize(image.getBuildRequest(this.project.getArtifact(), content));
|
return customize(image.getBuildRequest(this.project.getArtifact(), content));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean publishRegistryNotConfigured() {
|
||||||
|
return this.docker == null || this.docker.getPublishRegistry() == null
|
||||||
|
|| this.docker.getPublishRegistry().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
private TarArchive getApplicationContent(Owner owner, Libraries libraries) {
|
private TarArchive getApplicationContent(Owner owner, Libraries libraries) {
|
||||||
ImagePackager packager = getConfiguredPackager(() -> new ImagePackager(getJarFile()));
|
ImagePackager packager = getConfiguredPackager(() -> new ImagePackager(getJarFile()));
|
||||||
return new PackagedTarArchive(owner, libraries, packager);
|
return new PackagedTarArchive(owner, libraries, packager);
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ public class Docker {
|
||||||
|
|
||||||
private String certPath;
|
private String certPath;
|
||||||
|
|
||||||
private DockerRegistry registry;
|
private DockerRegistry builderRegistry;
|
||||||
|
|
||||||
|
private DockerRegistry publishRegistry;
|
||||||
|
|
||||||
public String getHost() {
|
public String getHost() {
|
||||||
return this.host;
|
return this.host;
|
||||||
|
|
@ -59,12 +61,30 @@ public class Docker {
|
||||||
this.certPath = certPath;
|
this.certPath = certPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DockerRegistry getBuilderRegistry() {
|
||||||
|
return this.builderRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link DockerRegistry} that configures registry authentication.
|
* Sets the {@link DockerRegistry} that configures authentication to the builder
|
||||||
* @param registry the registry configuration
|
* registry.
|
||||||
|
* @param builderRegistry the registry configuration
|
||||||
*/
|
*/
|
||||||
public void setRegistry(DockerRegistry registry) {
|
public void setBuilderRegistry(DockerRegistry builderRegistry) {
|
||||||
this.registry = registry;
|
this.builderRegistry = builderRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
DockerRegistry getPublishRegistry() {
|
||||||
|
return this.publishRegistry;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link DockerRegistry} that configures authentication to the publishing
|
||||||
|
* registry.
|
||||||
|
* @param builderRegistry the registry configuration
|
||||||
|
*/
|
||||||
|
public void setPublishRegistry(DockerRegistry builderRegistry) {
|
||||||
|
this.publishRegistry = builderRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -76,7 +96,8 @@ public class Docker {
|
||||||
DockerConfiguration asDockerConfiguration() {
|
DockerConfiguration asDockerConfiguration() {
|
||||||
DockerConfiguration dockerConfiguration = new DockerConfiguration();
|
DockerConfiguration dockerConfiguration = new DockerConfiguration();
|
||||||
dockerConfiguration = customizeHost(dockerConfiguration);
|
dockerConfiguration = customizeHost(dockerConfiguration);
|
||||||
dockerConfiguration = customizeAuthentication(dockerConfiguration);
|
dockerConfiguration = customizeBuilderAuthentication(dockerConfiguration);
|
||||||
|
dockerConfiguration = customizePublishAuthentication(dockerConfiguration);
|
||||||
return dockerConfiguration;
|
return dockerConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,19 +108,34 @@ public class Docker {
|
||||||
return dockerConfiguration;
|
return dockerConfiguration;
|
||||||
}
|
}
|
||||||
|
|
||||||
private DockerConfiguration customizeAuthentication(DockerConfiguration dockerConfiguration) {
|
private DockerConfiguration customizeBuilderAuthentication(DockerConfiguration dockerConfiguration) {
|
||||||
if (this.registry == null || this.registry.isEmpty()) {
|
if (this.builderRegistry == null || this.builderRegistry.isEmpty()) {
|
||||||
return dockerConfiguration;
|
return dockerConfiguration;
|
||||||
}
|
}
|
||||||
if (this.registry.hasTokenAuth() && !this.registry.hasUserAuth()) {
|
if (this.builderRegistry.hasTokenAuth() && !this.builderRegistry.hasUserAuth()) {
|
||||||
return dockerConfiguration.withRegistryTokenAuthentication(this.registry.getToken());
|
return dockerConfiguration.withBuilderRegistryTokenAuthentication(this.builderRegistry.getToken());
|
||||||
}
|
}
|
||||||
if (this.registry.hasUserAuth() && !this.registry.hasTokenAuth()) {
|
if (this.builderRegistry.hasUserAuth() && !this.builderRegistry.hasTokenAuth()) {
|
||||||
return dockerConfiguration.withRegistryUserAuthentication(this.registry.getUsername(),
|
return dockerConfiguration.withBuilderRegistryUserAuthentication(this.builderRegistry.getUsername(),
|
||||||
this.registry.getPassword(), this.registry.getUrl(), this.registry.getEmail());
|
this.builderRegistry.getPassword(), this.builderRegistry.getUrl(), this.builderRegistry.getEmail());
|
||||||
}
|
}
|
||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Invalid Docker registry configuration, either token or username/password must be provided");
|
"Invalid Docker builder registry configuration, either token or username/password must be provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
private DockerConfiguration customizePublishAuthentication(DockerConfiguration dockerConfiguration) {
|
||||||
|
if (this.publishRegistry == null || this.publishRegistry.isEmpty()) {
|
||||||
|
return dockerConfiguration;
|
||||||
|
}
|
||||||
|
if (this.publishRegistry.hasTokenAuth() && !this.publishRegistry.hasUserAuth()) {
|
||||||
|
return dockerConfiguration.withPublishRegistryTokenAuthentication(this.publishRegistry.getToken());
|
||||||
|
}
|
||||||
|
if (this.publishRegistry.hasUserAuth() && !this.publishRegistry.hasTokenAuth()) {
|
||||||
|
return dockerConfiguration.withPublishRegistryUserAuthentication(this.publishRegistry.getUsername(),
|
||||||
|
this.publishRegistry.getPassword(), this.publishRegistry.getUrl(), this.publishRegistry.getEmail());
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Invalid Docker publish registry configuration, either token or username/password must be provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -117,6 +153,20 @@ public class Docker {
|
||||||
|
|
||||||
private String token;
|
private String token;
|
||||||
|
|
||||||
|
public DockerRegistry() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public DockerRegistry(String username, String password, String url, String email) {
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.url = url;
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DockerRegistry(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
String getUsername() {
|
String getUsername() {
|
||||||
return this.username;
|
return this.username;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,11 @@ public class Image {
|
||||||
*/
|
*/
|
||||||
PullPolicy pullPolicy;
|
PullPolicy pullPolicy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the built image should be pushed to a registry.
|
||||||
|
*/
|
||||||
|
Boolean publish;
|
||||||
|
|
||||||
void setName(String name) {
|
void setName(String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
@ -89,6 +94,10 @@ public class Image {
|
||||||
this.pullPolicy = pullPolicy;
|
this.pullPolicy = pullPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setPublish(Boolean publish) {
|
||||||
|
this.publish = publish;
|
||||||
|
}
|
||||||
|
|
||||||
BuildRequest getBuildRequest(Artifact artifact, Function<Owner, TarArchive> applicationContent) {
|
BuildRequest getBuildRequest(Artifact artifact, Function<Owner, TarArchive> applicationContent) {
|
||||||
return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent));
|
return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent));
|
||||||
}
|
}
|
||||||
|
|
@ -116,6 +125,9 @@ public class Image {
|
||||||
if (this.pullPolicy != null) {
|
if (this.pullPolicy != null) {
|
||||||
request = request.withPullPolicy(this.pullPolicy);
|
request = request.withPullPolicy(this.pullPolicy);
|
||||||
}
|
}
|
||||||
|
if (this.publish != null) {
|
||||||
|
request = request.withPublish(this.publish);
|
||||||
|
}
|
||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost;
|
import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost;
|
||||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
|
|
||||||
import org.springframework.util.Base64Utils;
|
import org.springframework.util.Base64Utils;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
@ -38,7 +37,8 @@ public class DockerTests {
|
||||||
void asDockerConfigurationWithDefaults() {
|
void asDockerConfigurationWithDefaults() {
|
||||||
Docker docker = new Docker();
|
Docker docker = new Docker();
|
||||||
assertThat(docker.asDockerConfiguration().getHost()).isNull();
|
assertThat(docker.asDockerConfiguration().getHost()).isNull();
|
||||||
assertThat(docker.asDockerConfiguration().getRegistryAuthentication()).isNull();
|
assertThat(docker.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
|
||||||
|
assertThat(docker.asDockerConfiguration().getPublishRegistryAuthentication()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -52,50 +52,56 @@ public class DockerTests {
|
||||||
assertThat(host.getAddress()).isEqualTo("docker.example.com");
|
assertThat(host.getAddress()).isEqualTo("docker.example.com");
|
||||||
assertThat(host.isSecure()).isEqualTo(true);
|
assertThat(host.isSecure()).isEqualTo(true);
|
||||||
assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert");
|
assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert");
|
||||||
assertThat(docker.asDockerConfiguration().getRegistryAuthentication()).isNull();
|
assertThat(docker.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
|
||||||
|
assertThat(docker.asDockerConfiguration().getPublishRegistryAuthentication()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void asDockerConfigurationWithUserAuth() {
|
void asDockerConfigurationWithUserAuth() {
|
||||||
Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry();
|
|
||||||
dockerRegistry.setUsername("user");
|
|
||||||
dockerRegistry.setPassword("secret");
|
|
||||||
dockerRegistry.setUrl("https://docker.example.com");
|
|
||||||
dockerRegistry.setEmail("docker@example.com");
|
|
||||||
Docker docker = new Docker();
|
Docker docker = new Docker();
|
||||||
docker.setRegistry(dockerRegistry);
|
docker.setBuilderRegistry(
|
||||||
|
new Docker.DockerRegistry("user1", "secret1", "https://docker1.example.com", "docker1@example.com"));
|
||||||
|
docker.setPublishRegistry(
|
||||||
|
new Docker.DockerRegistry("user2", "secret2", "https://docker2.example.com", "docker2@example.com"));
|
||||||
DockerConfiguration dockerConfiguration = docker.asDockerConfiguration();
|
DockerConfiguration dockerConfiguration = docker.asDockerConfiguration();
|
||||||
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
|
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
|
||||||
assertThat(registryAuthentication).isNotNull();
|
.contains("\"username\" : \"user1\"").contains("\"password\" : \"secret1\"")
|
||||||
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
|
.contains("\"email\" : \"docker1@example.com\"")
|
||||||
.contains("\"username\" : \"user\"").contains("\"password\" : \"secret\"")
|
.contains("\"serveraddress\" : \"https://docker1.example.com\"");
|
||||||
.contains("\"email\" : \"docker@example.com\"")
|
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
|
||||||
.contains("\"serveraddress\" : \"https://docker.example.com\"");
|
.contains("\"username\" : \"user2\"").contains("\"password\" : \"secret2\"")
|
||||||
|
.contains("\"email\" : \"docker2@example.com\"")
|
||||||
|
.contains("\"serveraddress\" : \"https://docker2.example.com\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void asDockerConfigurationWithIncompleteUserAuthFails() {
|
void asDockerConfigurationWithIncompleteBuilderUserAuthFails() {
|
||||||
Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry();
|
|
||||||
dockerRegistry.setUsername("user");
|
|
||||||
dockerRegistry.setUrl("https://docker.example.com");
|
|
||||||
dockerRegistry.setEmail("docker@example.com");
|
|
||||||
Docker docker = new Docker();
|
Docker docker = new Docker();
|
||||||
docker.setRegistry(dockerRegistry);
|
docker.setBuilderRegistry(
|
||||||
|
new Docker.DockerRegistry("user", null, "https://docker.example.com", "docker@example.com"));
|
||||||
assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration)
|
assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration)
|
||||||
.withMessageContaining("Invalid Docker registry configuration");
|
.withMessageContaining("Invalid Docker builder registry configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void asDockerConfigurationWithIncompletePublishUserAuthFails() {
|
||||||
|
Docker docker = new Docker();
|
||||||
|
docker.setPublishRegistry(
|
||||||
|
new Docker.DockerRegistry("user", null, "https://docker.example.com", "docker@example.com"));
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration)
|
||||||
|
.withMessageContaining("Invalid Docker publish registry configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void asDockerConfigurationWithTokenAuth() {
|
void asDockerConfigurationWithTokenAuth() {
|
||||||
Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry();
|
|
||||||
dockerRegistry.setToken("token");
|
|
||||||
Docker docker = new Docker();
|
Docker docker = new Docker();
|
||||||
docker.setRegistry(dockerRegistry);
|
docker.setBuilderRegistry(new Docker.DockerRegistry("token1"));
|
||||||
|
docker.setPublishRegistry(new Docker.DockerRegistry("token2"));
|
||||||
DockerConfiguration dockerConfiguration = docker.asDockerConfiguration();
|
DockerConfiguration dockerConfiguration = docker.asDockerConfiguration();
|
||||||
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
|
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
|
||||||
assertThat(registryAuthentication).isNotNull();
|
.contains("\"identitytoken\" : \"token1\"");
|
||||||
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
|
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
|
||||||
.contains("\"identitytoken\" : \"token\"");
|
.contains("\"identitytoken\" : \"token2\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -105,9 +111,13 @@ public class DockerTests {
|
||||||
dockerRegistry.setPassword("secret");
|
dockerRegistry.setPassword("secret");
|
||||||
dockerRegistry.setToken("token");
|
dockerRegistry.setToken("token");
|
||||||
Docker docker = new Docker();
|
Docker docker = new Docker();
|
||||||
docker.setRegistry(dockerRegistry);
|
docker.setBuilderRegistry(dockerRegistry);
|
||||||
assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration)
|
assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration)
|
||||||
.withMessageContaining("Invalid Docker registry configuration");
|
.withMessageContaining("Invalid Docker builder registry configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
String decoded(String value) {
|
||||||
|
return new String(Base64Utils.decodeFromString(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,14 @@ class ImageTests {
|
||||||
assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.NEVER);
|
assertThat(request.getPullPolicy()).isEqualTo(PullPolicy.NEVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getBuildRequestWhenHasPublishUsesPublish() {
|
||||||
|
Image image = new Image();
|
||||||
|
image.publish = true;
|
||||||
|
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
|
||||||
|
assertThat(request.isPublish()).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
private Artifact createArtifact() {
|
private Artifact createArtifact() {
|
||||||
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
|
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
|
||||||
"jar", null, new DefaultArtifactHandler());
|
"jar", null, new DefaultArtifactHandler());
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue