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)));
|
||||
}
|
||||
|
||||
@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
|
||||
public void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume) {
|
||||
log(" > Executing lifecycle version " + version);
|
||||
|
|
|
|||
|
|
@ -92,11 +92,24 @@ public interface BuildLog {
|
|||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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.
|
||||
* @param request the build request
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ public class BuildRequest {
|
|||
|
||||
private final PullPolicy pullPolicy;
|
||||
|
||||
private final boolean publish;
|
||||
|
||||
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
Assert.notNull(applicationContent, "ApplicationContent must not be null");
|
||||
|
|
@ -70,12 +72,13 @@ public class BuildRequest {
|
|||
this.cleanCache = false;
|
||||
this.verboseLogging = false;
|
||||
this.pullPolicy = PullPolicy.ALWAYS;
|
||||
this.publish = false;
|
||||
this.creator = Creator.withVersion("");
|
||||
}
|
||||
|
||||
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
|
||||
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
|
||||
boolean verboseLogging, PullPolicy pullPolicy) {
|
||||
boolean verboseLogging, PullPolicy pullPolicy, boolean publish) {
|
||||
this.name = name;
|
||||
this.applicationContent = applicationContent;
|
||||
this.builder = builder;
|
||||
|
|
@ -85,6 +88,7 @@ public class BuildRequest {
|
|||
this.cleanCache = cleanCache;
|
||||
this.verboseLogging = verboseLogging;
|
||||
this.pullPolicy = pullPolicy;
|
||||
this.publish = publish;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -95,7 +99,7 @@ public class BuildRequest {
|
|||
public BuildRequest withBuilder(ImageReference builder) {
|
||||
Assert.notNull(builder, "Builder must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
|
||||
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy);
|
||||
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -105,7 +109,7 @@ public class BuildRequest {
|
|||
*/
|
||||
public BuildRequest withRunImage(ImageReference runImageName) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
|
||||
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy);
|
||||
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -116,7 +120,7 @@ public class BuildRequest {
|
|||
public BuildRequest withCreator(Creator creator) {
|
||||
Assert.notNull(creator, "Creator must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy);
|
||||
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -131,7 +135,7 @@ public class BuildRequest {
|
|||
Map<String, String> env = new LinkedHashMap<>(this.env);
|
||||
env.put(name, value);
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
|
||||
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy);
|
||||
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);
|
||||
updatedEnv.putAll(env);
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
|
||||
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy);
|
||||
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy,
|
||||
this.publish);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -154,7 +159,7 @@ public class BuildRequest {
|
|||
*/
|
||||
public BuildRequest withCleanCache(boolean cleanCache) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
cleanCache, this.verboseLogging, this.pullPolicy);
|
||||
cleanCache, this.verboseLogging, this.pullPolicy, this.publish);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -164,7 +169,7 @@ public class BuildRequest {
|
|||
*/
|
||||
public BuildRequest withVerboseLogging(boolean verboseLogging) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, verboseLogging, this.pullPolicy);
|
||||
this.cleanCache, verboseLogging, this.pullPolicy, this.publish);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -174,7 +179,17 @@ public class BuildRequest {
|
|||
*/
|
||||
public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
|
||||
this.cleanCache, this.verboseLogging, pullPolicy);
|
||||
this.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 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 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.TotalProgressEvent;
|
||||
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.configuration.DockerConfiguration;
|
||||
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
|
||||
|
|
@ -45,6 +46,8 @@ public class Builder {
|
|||
|
||||
private final DockerApi docker;
|
||||
|
||||
private final DockerConfiguration dockerConfiguration;
|
||||
|
||||
/**
|
||||
* Create a new builder instance.
|
||||
*/
|
||||
|
|
@ -66,7 +69,7 @@ public class Builder {
|
|||
* @param log a logger used to record output
|
||||
*/
|
||||
public Builder(BuildLog log) {
|
||||
this(log, new DockerApi());
|
||||
this(log, new DockerApi(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -76,13 +79,14 @@ public class Builder {
|
|||
* @since 2.4.0
|
||||
*/
|
||||
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");
|
||||
this.log = log;
|
||||
this.docker = docker;
|
||||
this.dockerConfiguration = dockerConfiguration;
|
||||
}
|
||||
|
||||
public void build(BuildRequest request) throws DockerEngineException, IOException {
|
||||
|
|
@ -97,6 +101,9 @@ public class Builder {
|
|||
this.docker.image().load(builder.getArchive(), UpdateListener.none());
|
||||
try {
|
||||
executeLifecycle(request, builder);
|
||||
if (request.isPublish()) {
|
||||
pushImage(request.getName());
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.docker.image().remove(builder.getName(), true);
|
||||
|
|
@ -143,11 +150,28 @@ public class Builder {
|
|||
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
|
||||
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingImage(reference, imageType);
|
||||
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);
|
||||
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) {
|
||||
StackId runImageStackId = StackId.fromImage(runImage);
|
||||
StackId builderImageStackId = StackId.fromImage(builderImage);
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ public class DockerApi {
|
|||
* @since 2.4.0
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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(listener, "Listener must not be null");
|
||||
URI createUri = buildUrl("/images/create", "fromImage", reference.toString());
|
||||
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
|
||||
listener.onStart();
|
||||
try {
|
||||
try (Response response = http().post(createUri)) {
|
||||
try (Response response = http().post(createUri, registryAuth)) {
|
||||
jsonStream().get(response.getContent(), PullImageUpdateEvent.class, (event) -> {
|
||||
digestCapture.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.
|
||||
* @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.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class PullImageUpdateEvent extends ProgressUpdateEvent {
|
||||
|
||||
private final String id;
|
||||
public class PullImageUpdateEvent extends ImageProgressUpdateEvent {
|
||||
|
||||
@JsonCreator
|
||||
public PullImageUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress) {
|
||||
super(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;
|
||||
super(id, status, progressDetail, progress);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
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 pull operation
|
||||
* and publishes {@link TotalProgressEvent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @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 final Consumer<TotalProgressEvent> consumer;
|
||||
|
||||
private boolean progressStarted;
|
||||
private static final String[] TRACKED_STATUS_KEYS = { "Downloading", "Extracting" };
|
||||
|
||||
/**
|
||||
* Create a new {@link TotalProgressPullListener} that prints a progress bar to
|
||||
|
|
@ -53,87 +46,7 @@ public class TotalProgressPullListener implements UpdateListener<PullImageUpdate
|
|||
* events}
|
||||
*/
|
||||
public TotalProgressPullListener(Consumer<TotalProgressEvent> consumer) {
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
super(consumer, TRACKED_STATUS_KEYS);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 DockerRegistryAuthentication authentication;
|
||||
private final DockerRegistryAuthentication builderAuthentication;
|
||||
|
||||
private final DockerRegistryAuthentication publishAuthentication;
|
||||
|
||||
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.authentication = authentication;
|
||||
this.builderAuthentication = builderAuthentication;
|
||||
this.publishAuthentication = publishAuthentication;
|
||||
}
|
||||
|
||||
public DockerHost getHost() {
|
||||
return this.host;
|
||||
}
|
||||
|
||||
public DockerRegistryAuthentication getRegistryAuthentication() {
|
||||
return this.authentication;
|
||||
public DockerRegistryAuthentication getBuilderRegistryAuthentication() {
|
||||
return this.builderAuthentication;
|
||||
}
|
||||
|
||||
public DockerRegistryAuthentication getPublishRegistryAuthentication() {
|
||||
return this.publishAuthentication;
|
||||
}
|
||||
|
||||
public DockerConfiguration withHost(String address, boolean secure, String certificatePath) {
|
||||
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");
|
||||
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) {
|
||||
Assert.notNull(username, "Username 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;
|
||||
|
||||
protected DockerHost(String address, boolean secure, String certificatePath) {
|
||||
public DockerHost(String address, boolean secure, String certificatePath) {
|
||||
this.address = address;
|
||||
this.secure = secure;
|
||||
this.certificatePath = certificatePath;
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ package org.springframework.boot.buildpack.platform.docker.configuration;
|
|||
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
|
||||
*/
|
||||
String createAuthHeader();
|
||||
String getAuthHeader();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class DockerRegistryTokenAuthentication extends JsonEncodedDockerRegistryAuthent
|
|||
|
||||
DockerRegistryTokenAuthentication(String token) {
|
||||
this.token = token;
|
||||
createAuthHeader();
|
||||
}
|
||||
|
||||
String getToken() {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class DockerRegistryUserAuthentication extends JsonEncodedDockerRegistryAuthenti
|
|||
this.password = password;
|
||||
this.url = url;
|
||||
this.email = email;
|
||||
createAuthHeader();
|
||||
}
|
||||
|
||||
String getUsername() {
|
||||
|
|
|
|||
|
|
@ -22,17 +22,23 @@ import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
|||
import org.springframework.util.Base64Utils;
|
||||
|
||||
/**
|
||||
* {@link DockerRegistryAuthentication} that uses creates a Base64 encoded auth header
|
||||
* value based on the JSON created from the instance.
|
||||
* {@link DockerRegistryAuthentication} that uses a Base64 encoded auth header value based
|
||||
* on the JSON created from the instance.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class JsonEncodedDockerRegistryAuthentication implements DockerRegistryAuthentication {
|
||||
|
||||
private String authHeader;
|
||||
|
||||
@Override
|
||||
public String createAuthHeader() {
|
||||
public String getAuthHeader() {
|
||||
return this.authHeader;
|
||||
}
|
||||
|
||||
protected void createAuthHeader() {
|
||||
try {
|
||||
return Base64Utils.encodeToUrlSafeString(SharedObjectMapper.get().writeValueAsBytes(this));
|
||||
this.authHeader = Base64Utils.encodeToUrlSafeString(SharedObjectMapper.get().writeValueAsBytes(this));
|
||||
}
|
||||
catch (JsonProcessingException 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.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.IOConsumer;
|
||||
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
||||
|
|
@ -53,19 +52,17 @@ import org.springframework.util.StringUtils;
|
|||
*/
|
||||
abstract class HttpClientTransport implements HttpTransport {
|
||||
|
||||
static final String REGISTRY_AUTH_HEADER = "X-Registry-Auth";
|
||||
|
||||
private final CloseableHttpClient client;
|
||||
|
||||
private final HttpHost host;
|
||||
|
||||
private final String registryAuthHeader;
|
||||
|
||||
protected HttpClientTransport(CloseableHttpClient client, HttpHost host,
|
||||
DockerRegistryAuthentication authentication) {
|
||||
protected HttpClientTransport(CloseableHttpClient client, HttpHost host) {
|
||||
Assert.notNull(client, "Client must not be null");
|
||||
Assert.notNull(host, "Host must not be null");
|
||||
this.client = client;
|
||||
this.host = host;
|
||||
this.registryAuthHeader = buildRegistryAuthHeader(authentication);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -88,6 +85,17 @@ abstract class HttpClientTransport implements HttpTransport {
|
|||
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.
|
||||
* @param uri the destination URI
|
||||
|
|
@ -122,11 +130,6 @@ abstract class HttpClientTransport implements HttpTransport {
|
|||
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,
|
||||
IOConsumer<OutputStream> writer) {
|
||||
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
|
||||
|
|
@ -134,11 +137,15 @@ abstract class HttpClientTransport implements HttpTransport {
|
|||
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) {
|
||||
try {
|
||||
if (this.registryAuthHeader != null) {
|
||||
request.addHeader("X-Registry-Auth", this.registryAuthHeader);
|
||||
}
|
||||
CloseableHttpResponse response = this.client.execute(this.host, request);
|
||||
StatusLine statusLine = response.getStatusLine();
|
||||
int statusCode = statusLine.getStatusCode();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import java.io.OutputStream;
|
|||
import java.net.URI;
|
||||
|
||||
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.system.Environment;
|
||||
|
||||
|
|
@ -51,6 +52,15 @@ public interface HttpTransport {
|
|||
*/
|
||||
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.
|
||||
* @param uri the destination URI (excluding any host/port)
|
||||
|
|
@ -85,17 +95,17 @@ public interface HttpTransport {
|
|||
* @return a {@link HttpTransport} instance
|
||||
*/
|
||||
static HttpTransport create() {
|
||||
return create(new DockerConfiguration());
|
||||
return create(Environment.SYSTEM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the most suitable {@link HttpTransport} based on the
|
||||
* {@link Environment#SYSTEM system environment}.
|
||||
* @param dockerConfiguration the Docker engine configuration
|
||||
* @param dockerHost the Docker engine host configuration
|
||||
* @return a {@link HttpTransport} instance
|
||||
*/
|
||||
static HttpTransport create(DockerConfiguration dockerConfiguration) {
|
||||
return create(Environment.SYSTEM, dockerConfiguration);
|
||||
static HttpTransport create(DockerHost dockerHost) {
|
||||
return create(Environment.SYSTEM, dockerHost);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -105,19 +115,19 @@ public interface HttpTransport {
|
|||
* @return a {@link HttpTransport} instance
|
||||
*/
|
||||
static HttpTransport create(Environment environment) {
|
||||
return create(environment, new DockerConfiguration());
|
||||
return create(environment, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the most suitable {@link HttpTransport} based on the given
|
||||
* {@link Environment} and {@link DockerConfiguration}.
|
||||
* @param environment the source environment
|
||||
* @param dockerConfiguration the Docker engine configuration
|
||||
* @param dockerHost the Docker engine host configuration
|
||||
* @return a {@link HttpTransport} instance
|
||||
*/
|
||||
static HttpTransport create(Environment environment, DockerConfiguration dockerConfiguration) {
|
||||
HttpTransport remote = RemoteHttpClientTransport.createIfPossible(environment, dockerConfiguration);
|
||||
return (remote != null) ? remote : LocalHttpClientTransport.create(environment, dockerConfiguration);
|
||||
static HttpTransport create(Environment environment, DockerHost dockerHost) {
|
||||
HttpTransport remote = RemoteHttpClientTransport.createIfPossible(environment, dockerHost);
|
||||
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.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.NamedPipeSocket;
|
||||
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 LocalHttpClientTransport(CloseableHttpClient client, DockerRegistryAuthentication authentication) {
|
||||
super(client, LOCAL_DOCKER_HOST, authentication);
|
||||
private LocalHttpClientTransport(CloseableHttpClient client) {
|
||||
super(client, LOCAL_DOCKER_HOST);
|
||||
}
|
||||
|
||||
static LocalHttpClientTransport create(Environment environment, DockerConfiguration dockerConfiguration) {
|
||||
static LocalHttpClientTransport create(Environment environment) {
|
||||
HttpClientBuilder builder = HttpClients.custom();
|
||||
builder.setConnectionManager(new LocalConnectionManager(socketFilePath(environment)));
|
||||
builder.setSchemePortResolver(new LocalSchemePortResolver());
|
||||
return new LocalHttpClientTransport(builder.build(),
|
||||
(dockerConfiguration != null) ? dockerConfiguration.getRegistryAuthentication() : null);
|
||||
return new LocalHttpClientTransport(builder.build());
|
||||
}
|
||||
|
||||
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.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.DockerRegistryAuthentication;
|
||||
import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory;
|
||||
import org.springframework.boot.buildpack.platform.system.Environment;
|
||||
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 RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host,
|
||||
DockerRegistryAuthentication authentication) {
|
||||
super(client, host, authentication);
|
||||
private RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host) {
|
||||
super(client, host);
|
||||
}
|
||||
|
||||
static RemoteHttpClientTransport createIfPossible(Environment environment,
|
||||
DockerConfiguration dockerConfiguration) {
|
||||
return createIfPossible(environment, dockerConfiguration, new SslContextFactory());
|
||||
static RemoteHttpClientTransport createIfPossible(Environment environment, DockerHost dockerHost) {
|
||||
return createIfPossible(environment, dockerHost, new SslContextFactory());
|
||||
}
|
||||
|
||||
static RemoteHttpClientTransport createIfPossible(Environment environment, DockerConfiguration dockerConfiguration,
|
||||
static RemoteHttpClientTransport createIfPossible(Environment environment, DockerHost dockerHost,
|
||||
SslContextFactory sslContextFactory) {
|
||||
DockerHost host = getHost(environment, dockerConfiguration);
|
||||
DockerHost host = getHost(environment, dockerHost);
|
||||
if (host == null || host.getAddress() == null || isLocalFileReference(host.getAddress())) {
|
||||
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) {
|
||||
|
|
@ -80,16 +76,15 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
|
|||
}
|
||||
}
|
||||
|
||||
private static RemoteHttpClientTransport create(DockerHost host, DockerConfiguration dockerConfiguration,
|
||||
SslContextFactory sslContextFactory, HttpHost tcpHost) {
|
||||
private static RemoteHttpClientTransport create(DockerHost host, SslContextFactory sslContextFactory,
|
||||
HttpHost tcpHost) {
|
||||
HttpClientBuilder builder = HttpClients.custom();
|
||||
if (host.isSecure()) {
|
||||
builder.setSSLSocketFactory(getSecureConnectionSocketFactory(host, sslContextFactory));
|
||||
}
|
||||
String scheme = host.isSecure() ? "https" : "http";
|
||||
HttpHost httpHost = new HttpHost(tcpHost.getHostName(), tcpHost.getPort(), scheme);
|
||||
return new RemoteHttpClientTransport(builder.build(), httpHost,
|
||||
(dockerConfiguration != null) ? dockerConfiguration.getRegistryAuthentication() : null);
|
||||
return new RemoteHttpClientTransport(builder.build(), httpHost);
|
||||
}
|
||||
|
||||
private static LayeredConnectionSocketFactory getSecureConnectionSocketFactory(DockerHost host,
|
||||
|
|
@ -101,14 +96,11 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
|
|||
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) {
|
||||
return new EnvironmentDockerHost(environment);
|
||||
}
|
||||
if (dockerConfiguration != null && dockerConfiguration.getHost() != null) {
|
||||
return dockerConfiguration.getHost();
|
||||
}
|
||||
return null;
|
||||
return 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.VolumeApi;
|
||||
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.type.ContainerReference;
|
||||
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.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link Builder}.
|
||||
|
|
@ -83,18 +86,53 @@ class BuilderTests {
|
|||
DockerApi docker = mockDockerApi();
|
||||
Image builderImage = loadImage("image.json");
|
||||
Image runImage = loadImage("run-image.json");
|
||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||
.willAnswer(withPulledImage(builderImage));
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any()))
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
|
||||
.willAnswer(withPulledImage(runImage));
|
||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
||||
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||
BuildRequest request = getTestRequest();
|
||||
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(), 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()).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
|
||||
|
|
@ -103,11 +141,11 @@ class BuilderTests {
|
|||
DockerApi docker = mockDockerApi();
|
||||
Image builderImage = loadImage("image-with-no-run-image-tag.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));
|
||||
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));
|
||||
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"));
|
||||
builder.build(request);
|
||||
assertThat(out.toString()).contains("Running creator");
|
||||
|
|
@ -123,12 +161,12 @@ class BuilderTests {
|
|||
DockerApi docker = mockDockerApi();
|
||||
Image builderImage = loadImage("image-with-run-image-digest.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));
|
||||
given(docker.image().pull(eq(ImageReference.of(
|
||||
"docker.io/cloudfoundry/run@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d")),
|
||||
any())).willAnswer(withPulledImage(runImage));
|
||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
||||
any(), isNull())).willAnswer(withPulledImage(runImage));
|
||||
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||
BuildRequest request = getTestRequest();
|
||||
builder.build(request);
|
||||
assertThat(out.toString()).contains("Running creator");
|
||||
|
|
@ -144,11 +182,11 @@ class BuilderTests {
|
|||
DockerApi docker = mockDockerApi();
|
||||
Image builderImage = loadImage("image.json");
|
||||
Image runImage = loadImage("run-image.json");
|
||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||
.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));
|
||||
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"));
|
||||
builder.build(request);
|
||||
assertThat(out.toString()).contains("Running creator");
|
||||
|
|
@ -164,15 +202,15 @@ class BuilderTests {
|
|||
DockerApi docker = mockDockerApi();
|
||||
Image builderImage = loadImage("image.json");
|
||||
Image runImage = loadImage("run-image.json");
|
||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||
.willAnswer(withPulledImage(builderImage));
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any()))
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
|
||||
.willAnswer(withPulledImage(runImage));
|
||||
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME))))
|
||||
.willReturn(builderImage);
|
||||
given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb"))))
|
||||
.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);
|
||||
builder.build(request);
|
||||
assertThat(out.toString()).contains("Running creator");
|
||||
|
|
@ -190,15 +228,15 @@ class BuilderTests {
|
|||
DockerApi docker = mockDockerApi();
|
||||
Image builderImage = loadImage("image.json");
|
||||
Image runImage = loadImage("run-image.json");
|
||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||
.willAnswer(withPulledImage(builderImage));
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any()))
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
|
||||
.willAnswer(withPulledImage(runImage));
|
||||
given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME))))
|
||||
.willReturn(builderImage);
|
||||
given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb"))))
|
||||
.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);
|
||||
builder.build(request);
|
||||
assertThat(out.toString()).contains("Running creator");
|
||||
|
|
@ -206,7 +244,7 @@ class BuilderTests {
|
|||
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
|
||||
verify(docker.image()).load(archive.capture(), any());
|
||||
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());
|
||||
}
|
||||
|
||||
|
|
@ -216,9 +254,9 @@ class BuilderTests {
|
|||
DockerApi docker = mockDockerApi();
|
||||
Image builderImage = loadImage("image.json");
|
||||
Image runImage = loadImage("run-image.json");
|
||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any()))
|
||||
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
|
||||
.willAnswer(withPulledImage(builderImage));
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any()))
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
|
||||
.willAnswer(withPulledImage(runImage));
|
||||
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))
|
||||
|
|
@ -226,7 +264,7 @@ class BuilderTests {
|
|||
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))
|
||||
.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);
|
||||
builder.build(request);
|
||||
assertThat(out.toString()).contains("Running creator");
|
||||
|
|
@ -235,7 +273,7 @@ class BuilderTests {
|
|||
verify(docker.image()).load(archive.capture(), any());
|
||||
verify(docker.image()).remove(archive.getValue().getTag(), true);
|
||||
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
|
||||
|
|
@ -244,11 +282,11 @@ class BuilderTests {
|
|||
DockerApi docker = mockDockerApi();
|
||||
Image builderImage = loadImage("image.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));
|
||||
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));
|
||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
||||
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||
BuildRequest request = getTestRequest();
|
||||
assertThatIllegalStateException().isThrownBy(() -> builder.build(request)).withMessage(
|
||||
"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();
|
||||
Image builderImage = loadImage("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));
|
||||
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));
|
||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
||||
Builder builder = new Builder(BuildLog.to(out), docker, null);
|
||||
BuildRequest request = getTestRequest();
|
||||
assertThatExceptionOfType(BuilderException.class).isThrownBy(() -> builder.build(request))
|
||||
.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.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
|
@ -127,6 +128,9 @@ class DockerApiTests {
|
|||
@Mock
|
||||
private UpdateListener<PullImageUpdateEvent> pullListener;
|
||||
|
||||
@Mock
|
||||
private UpdateListener<PushImageUpdateEvent> pushListener;
|
||||
|
||||
@Mock
|
||||
private UpdateListener<LoadImageUpdateEvent> loadListener;
|
||||
|
||||
|
|
@ -156,7 +160,7 @@ class DockerApiTests {
|
|||
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(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"));
|
||||
Image image = this.api.pull(reference, this.pullListener);
|
||||
assertThat(image.getLayers()).hasSize(46);
|
||||
|
|
@ -166,6 +170,57 @@ class DockerApiTests {
|
|||
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
|
||||
void loadWhenArchiveIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.load(null, UpdateListener.none()))
|
||||
|
|
|
|||
|
|
@ -26,17 +26,18 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
* Tests for {@link LoadImageUpdateEvent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class LoadImageUpdateEventTests extends ProgressUpdateEventTests {
|
||||
class LoadImageUpdateEventTests extends ProgressUpdateEventTests<LoadImageUpdateEvent> {
|
||||
|
||||
@Test
|
||||
void getStreamReturnsStream() {
|
||||
LoadImageUpdateEvent event = (LoadImageUpdateEvent) createEvent();
|
||||
LoadImageUpdateEvent event = createEvent();
|
||||
assertThat(event.getStream()).isEqualTo("stream");
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,8 +26,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
* Tests for {@link ProgressUpdateEvent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
abstract class ProgressUpdateEventTests {
|
||||
abstract class ProgressUpdateEventTests<E extends ProgressUpdateEvent> {
|
||||
|
||||
@Test
|
||||
void getStatusReturnsStatus() {
|
||||
|
|
@ -66,10 +67,10 @@ abstract class ProgressUpdateEventTests {
|
|||
assertThat(ProgressDetail.isEmpty(detail)).isFalse();
|
||||
}
|
||||
|
||||
protected ProgressUpdateEvent createEvent() {
|
||||
protected E createEvent() {
|
||||
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}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class PullImageUpdateEventTests extends ProgressUpdateEventTests {
|
||||
class PullImageUpdateEventTests extends ProgressUpdateEventTests<PullImageUpdateEvent> {
|
||||
|
||||
@Test
|
||||
void getIdReturnsId() {
|
||||
PullImageUpdateEvent event = (PullImageUpdateEvent) createEvent();
|
||||
PullImageUpdateEvent event = createEvent();
|
||||
assertThat(event.getId()).isEqualTo("id");
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.function.Consumer;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
|
|
@ -33,13 +34,14 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
* Tests for {@link TotalProgressPullListener}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class TotalProgressPullListenerTests extends AbstractJsonTests {
|
||||
class TotalProgressListenerTests extends AbstractJsonTests {
|
||||
|
||||
@Test
|
||||
void totalProgress() throws Exception {
|
||||
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);
|
||||
int last = 0;
|
||||
for (Integer update : progress) {
|
||||
|
|
@ -52,26 +54,25 @@ class TotalProgressPullListenerTests extends AbstractJsonTests {
|
|||
@Test
|
||||
@Disabled("For visual inspection")
|
||||
void totalProgressUpdatesSmoothly() throws Exception {
|
||||
TestTotalProgressPullListener listener = new TestTotalProgressPullListener(
|
||||
new TotalProgressBar("Pulling layers:"));
|
||||
TestTotalProgressListener listener = new TestTotalProgressListener(new TotalProgressBar("Pulling layers:"));
|
||||
run(listener);
|
||||
}
|
||||
|
||||
private void run(TotalProgressPullListener listener) throws IOException {
|
||||
private void run(TestTotalProgressListener listener) throws IOException {
|
||||
JsonStream jsonStream = new JsonStream(getObjectMapper());
|
||||
listener.onStart();
|
||||
jsonStream.get(getContent("pull-stream.json"), PullImageUpdateEvent.class, listener::onUpdate);
|
||||
jsonStream.get(getContent("pull-stream.json"), TestImageUpdateEvent.class, listener::onUpdate);
|
||||
listener.onFinish();
|
||||
}
|
||||
|
||||
private static class TestTotalProgressPullListener extends TotalProgressPullListener {
|
||||
private static class TestTotalProgressListener extends TotalProgressListener<TestImageUpdateEvent> {
|
||||
|
||||
TestTotalProgressPullListener(Consumer<TotalProgressEvent> consumer) {
|
||||
super(consumer);
|
||||
TestTotalProgressListener(Consumer<TotalProgressEvent> consumer) {
|
||||
super(consumer, new String[] { "Pulling", "Downloading", "Extracting" });
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(PullImageUpdateEvent event) {
|
||||
public void onUpdate(TestImageUpdateEvent event) {
|
||||
super.onUpdate(event);
|
||||
try {
|
||||
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
|
||||
void createDockerConfigurationWithDefaults() {
|
||||
DockerConfiguration configuration = new DockerConfiguration();
|
||||
assertThat(configuration.getRegistryAuthentication()).isNull();
|
||||
assertThat(configuration.getBuilderRegistryAuthentication()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createDockerConfigurationWithUserAuth() {
|
||||
DockerConfiguration configuration = new DockerConfiguration().withRegistryUserAuthentication("user", "secret",
|
||||
"https://docker.example.com", "docker@example.com");
|
||||
DockerRegistryAuthentication auth = configuration.getRegistryAuthentication();
|
||||
DockerConfiguration configuration = new DockerConfiguration().withBuilderRegistryUserAuthentication("user",
|
||||
"secret", "https://docker.example.com", "docker@example.com");
|
||||
DockerRegistryAuthentication auth = configuration.getBuilderRegistryAuthentication();
|
||||
assertThat(auth).isNotNull();
|
||||
assertThat(auth).isInstanceOf(DockerRegistryUserAuthentication.class);
|
||||
DockerRegistryUserAuthentication userAuth = (DockerRegistryUserAuthentication) auth;
|
||||
|
|
@ -50,8 +50,8 @@ public class DockerConfigurationTests {
|
|||
|
||||
@Test
|
||||
void createDockerConfigurationWithTokenAuth() {
|
||||
DockerConfiguration configuration = new DockerConfiguration().withRegistryTokenAuthentication("token");
|
||||
DockerRegistryAuthentication auth = configuration.getRegistryAuthentication();
|
||||
DockerConfiguration configuration = new DockerConfiguration().withBuilderRegistryTokenAuthentication("token");
|
||||
DockerRegistryAuthentication auth = configuration.getBuilderRegistryAuthentication();
|
||||
assertThat(auth).isNotNull();
|
||||
assertThat(auth).isInstanceOf(DockerRegistryTokenAuthentication.class);
|
||||
DockerRegistryTokenAuthentication tokenAuth = (DockerRegistryTokenAuthentication) auth;
|
||||
|
|
|
|||
|
|
@ -29,13 +29,15 @@ import org.springframework.util.StreamUtils;
|
|||
|
||||
/**
|
||||
* Tests for {@link DockerRegistryTokenAuthentication}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class DockerRegistryTokenAuthenticationTests extends AbstractJsonTests {
|
||||
|
||||
@Test
|
||||
void createAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
|
||||
DockerRegistryTokenAuthentication auth = new DockerRegistryTokenAuthentication("tokenvalue");
|
||||
String header = auth.createAuthHeader();
|
||||
String header = auth.getAuthHeader();
|
||||
String expectedJson = StreamUtils.copyToString(getContent("auth-token.json"), StandardCharsets.UTF_8);
|
||||
JSONAssert.assertEquals(expectedJson, new String(Base64Utils.decodeFromUrlSafeString(header)), false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ import org.springframework.util.StreamUtils;
|
|||
|
||||
/**
|
||||
* Tests for {@link DockerRegistryUserAuthentication}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class DockerRegistryUserAuthenticationTests extends AbstractJsonTests {
|
||||
|
||||
|
|
@ -36,13 +38,13 @@ class DockerRegistryUserAuthenticationTests extends AbstractJsonTests {
|
|||
void createMinimalAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
|
||||
DockerRegistryUserAuthentication auth = new DockerRegistryUserAuthentication("user", "secret",
|
||||
"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
|
||||
void createFullAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import java.io.InputStream;
|
|||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpEntityEnclosingRequest;
|
||||
import org.apache.http.HttpHeaders;
|
||||
|
|
@ -44,9 +43,7 @@ import org.mockito.Captor;
|
|||
import org.mockito.Mock;
|
||||
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.util.Base64Utils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
|
@ -123,6 +120,37 @@ class HttpClientTransportTests {
|
|||
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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
|
|
@ -237,47 +265,6 @@ class HttpClientTransportTests {
|
|||
.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 {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
entity.writeTo(out);
|
||||
|
|
@ -296,11 +283,7 @@ class HttpClientTransportTests {
|
|||
static class TestHttpClientTransport extends HttpClientTransport {
|
||||
|
||||
protected TestHttpClientTransport(CloseableHttpClient client) {
|
||||
super(client, HttpHost.create("docker://localhost"), null);
|
||||
}
|
||||
|
||||
protected TestHttpClientTransport(CloseableHttpClient client, DockerConfiguration dockerConfiguration) {
|
||||
super(client, HttpHost.create("docker://localhost"), dockerConfiguration.getRegistryAuthentication());
|
||||
super(client, HttpHost.create("docker://localhost"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
|
|||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
|
|
@ -52,7 +53,7 @@ class RemoteHttpClientTransportTests {
|
|||
@Test
|
||||
void createIfPossibleWhenDockerHostIsNotSetReturnsNull() {
|
||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
||||
this.dockerConfiguration);
|
||||
new DockerHost(null, false, null));
|
||||
assertThat(transport).isNull();
|
||||
}
|
||||
|
||||
|
|
@ -67,8 +68,7 @@ class RemoteHttpClientTransportTests {
|
|||
String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath()
|
||||
.toString();
|
||||
this.environment.put("DOCKER_HOST", dummySocketFilePath);
|
||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
||||
this.dockerConfiguration);
|
||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, null);
|
||||
assertThat(transport).isNull();
|
||||
}
|
||||
|
||||
|
|
@ -77,22 +77,21 @@ class RemoteHttpClientTransportTests {
|
|||
String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath()
|
||||
.toString();
|
||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
||||
this.dockerConfiguration.withHost(dummySocketFilePath, false, null));
|
||||
new DockerHost(dummySocketFilePath, false, null));
|
||||
assertThat(transport).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createIfPossibleWhenDockerHostInEnvironmentIsAddressReturnsTransport() {
|
||||
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
|
||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
||||
this.dockerConfiguration);
|
||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, null);
|
||||
assertThat(transport).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createIfPossibleWhenDockerHostInConfigurationIsAddressReturnsTransport() {
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -100,8 +99,8 @@ class RemoteHttpClientTransportTests {
|
|||
void createIfPossibleWhenTlsVerifyInEnvironmentWithMissingCertPathThrowsException() {
|
||||
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
|
||||
this.environment.put("DOCKER_TLS_VERIFY", "1");
|
||||
assertThatIllegalArgumentException().isThrownBy(
|
||||
() -> RemoteHttpClientTransport.createIfPossible(this.environment::get, this.dockerConfiguration))
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> RemoteHttpClientTransport.createIfPossible(this.environment::get, null))
|
||||
.withMessageContaining("Docker host TLS verification requires trust material");
|
||||
}
|
||||
|
||||
|
|
@ -109,15 +108,14 @@ class RemoteHttpClientTransportTests {
|
|||
void createIfPossibleWhenTlsVerifyInConfigurationWithMissingCertPathThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.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");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createIfPossibleWhenNoTlsVerifyUsesHttp() {
|
||||
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
|
||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
|
||||
this.dockerConfiguration);
|
||||
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, null);
|
||||
assertThat(transport.getHost()).satisfies(hostOf("http", "192.168.1.2", 2376));
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +127,7 @@ class RemoteHttpClientTransportTests {
|
|||
SslContextFactory sslContextFactory = mock(SslContextFactory.class);
|
||||
given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault());
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
@ -138,20 +136,11 @@ class RemoteHttpClientTransportTests {
|
|||
SslContextFactory sslContextFactory = mock(SslContextFactory.class);
|
||||
given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault());
|
||||
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);
|
||||
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) {
|
||||
return (host) -> {
|
||||
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.junit.jupiter:junit-jupiter")
|
||||
testImplementation("org.mockito:mockito-core")
|
||||
testImplementation("org.testcontainers:junit-jupiter")
|
||||
testImplementation("org.testcontainers:testcontainers")
|
||||
|
||||
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]]
|
||||
=== 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.
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
|
|
@ -133,6 +136,11 @@ Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`.
|
|||
|
|
||||
| Enables verbose logging of builder operations.
|
||||
| `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.
|
||||
|
|
@ -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
|
||||
----
|
||||
|
||||
[[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]]
|
||||
==== 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:
|
||||
|
|
@ -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]
|
||||
----
|
||||
|
||||
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"]
|
||||
.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]
|
||||
----
|
||||
|
||||
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"]
|
||||
.Groovy
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ bootJar {
|
|||
// tag::docker-auth-token[]
|
||||
bootBuildImage {
|
||||
docker {
|
||||
registry {
|
||||
builderRegistry {
|
||||
token = "9cbaf023786cd7..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ tasks.getByName<BootJar>("bootJar") {
|
|||
// tag::docker-auth-token[]
|
||||
tasks.getByName<BootBuildImage>("bootBuildImage") {
|
||||
docker {
|
||||
registry {
|
||||
builderRegistry {
|
||||
token = "9cbaf023786cd7..."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ bootJar {
|
|||
// tag::docker-auth-user[]
|
||||
bootBuildImage {
|
||||
docker {
|
||||
registry {
|
||||
builderRegistry {
|
||||
username = "user"
|
||||
password = "secret"
|
||||
url = "https://docker.example.com/v1/"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ tasks.getByName<BootJar>("bootJar") {
|
|||
// tag::docker-auth-user[]
|
||||
tasks.getByName<BootBuildImage>("bootBuildImage") {
|
||||
docker {
|
||||
registry {
|
||||
builderRegistry {
|
||||
username = "user"
|
||||
password = "secret"
|
||||
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 org.gradle.api.Action;
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.JavaVersion;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.Task;
|
||||
|
|
@ -76,6 +77,8 @@ public class BootBuildImage extends DefaultTask {
|
|||
|
||||
private PullPolicy pullPolicy;
|
||||
|
||||
private boolean publish;
|
||||
|
||||
private DockerSpec docker = new DockerSpec();
|
||||
|
||||
public BootBuildImage() {
|
||||
|
|
@ -252,6 +255,24 @@ public class BootBuildImage extends DefaultTask {
|
|||
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.
|
||||
* @return docker configuration.
|
||||
|
|
@ -312,6 +333,7 @@ public class BootBuildImage extends DefaultTask {
|
|||
request = request.withCleanCache(this.cleanCache);
|
||||
request = request.withVerboseLogging(this.verboseLogging);
|
||||
request = customizePullPolicy(request);
|
||||
request = customizePublish(request);
|
||||
return request;
|
||||
}
|
||||
|
||||
|
|
@ -354,6 +376,16 @@ public class BootBuildImage extends DefaultTask {
|
|||
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() {
|
||||
return this.targetJavaVersion.get().getMajorVersion() + ".*";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,14 +41,18 @@ public class DockerSpec {
|
|||
|
||||
private String certPath;
|
||||
|
||||
private final DockerRegistrySpec registry;
|
||||
private final DockerRegistrySpec builderRegistry;
|
||||
|
||||
private final DockerRegistrySpec publishRegistry;
|
||||
|
||||
public DockerSpec() {
|
||||
this.registry = new DockerRegistrySpec();
|
||||
this.builderRegistry = new DockerRegistrySpec();
|
||||
this.publishRegistry = new DockerRegistrySpec();
|
||||
}
|
||||
|
||||
DockerSpec(DockerRegistrySpec registry) {
|
||||
this.registry = registry;
|
||||
DockerSpec(DockerRegistrySpec builderRegistry, DockerRegistrySpec publishRegistry) {
|
||||
this.builderRegistry = builderRegistry;
|
||||
this.publishRegistry = publishRegistry;
|
||||
}
|
||||
|
||||
@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
|
||||
*/
|
||||
@Nested
|
||||
public DockerRegistrySpec getRegistry() {
|
||||
return this.registry;
|
||||
public DockerRegistrySpec getBuilderRegistry() {
|
||||
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
|
||||
*/
|
||||
public void registry(Action<DockerRegistrySpec> action) {
|
||||
action.execute(this.registry);
|
||||
public void builderRegistry(Action<DockerRegistrySpec> action) {
|
||||
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
|
||||
*/
|
||||
public void registry(Closure<?> closure) {
|
||||
registry(ConfigureUtil.configureUsing(closure));
|
||||
public void builderRegistry(Closure<?> 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 dockerConfiguration = new DockerConfiguration();
|
||||
dockerConfiguration = customizeHost(dockerConfiguration);
|
||||
dockerConfiguration = customizeAuthentication(dockerConfiguration);
|
||||
dockerConfiguration = customizeBuilderAuthentication(dockerConfiguration);
|
||||
dockerConfiguration = customizePublishAuthentication(dockerConfiguration);
|
||||
return dockerConfiguration;
|
||||
}
|
||||
|
||||
|
|
@ -126,19 +162,34 @@ public class DockerSpec {
|
|||
return dockerConfiguration;
|
||||
}
|
||||
|
||||
private DockerConfiguration customizeAuthentication(DockerConfiguration dockerConfiguration) {
|
||||
if (this.registry == null || this.registry.hasEmptyAuth()) {
|
||||
private DockerConfiguration customizeBuilderAuthentication(DockerConfiguration dockerConfiguration) {
|
||||
if (this.builderRegistry == null || this.builderRegistry.hasEmptyAuth()) {
|
||||
return dockerConfiguration;
|
||||
}
|
||||
if (this.registry.hasTokenAuth() && !this.registry.hasUserAuth()) {
|
||||
return dockerConfiguration.withRegistryTokenAuthentication(this.registry.getToken());
|
||||
if (this.builderRegistry.hasTokenAuth() && !this.builderRegistry.hasUserAuth()) {
|
||||
return dockerConfiguration.withBuilderRegistryTokenAuthentication(this.builderRegistry.getToken());
|
||||
}
|
||||
if (this.registry.hasUserAuth() && !this.registry.hasTokenAuth()) {
|
||||
return dockerConfiguration.withRegistryUserAuthentication(this.registry.getUsername(),
|
||||
this.registry.getPassword(), this.registry.getUrl(), this.registry.getEmail());
|
||||
if (this.builderRegistry.hasUserAuth() && !this.builderRegistry.hasTokenAuth()) {
|
||||
return dockerConfiguration.withBuilderRegistryUserAuthentication(this.builderRegistry.getUsername(),
|
||||
this.builderRegistry.getPassword(), this.builderRegistry.getUrl(), this.builderRegistry.getEmail());
|
||||
}
|
||||
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;
|
||||
|
||||
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.
|
||||
* @return the registry username
|
||||
|
|
|
|||
|
|
@ -178,6 +178,15 @@ class BootBuildImageIntegrationTests {
|
|||
.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() {
|
||||
File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/main/java/example");
|
||||
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.Map;
|
||||
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.JavaVersion;
|
||||
import org.gradle.api.Project;
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link BootBuildImage}.
|
||||
|
|
@ -174,6 +176,18 @@ class BootBuildImageTests {
|
|||
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
|
||||
void whenNoBuilderIsConfiguredThenRequestHasDefaultBuilder() {
|
||||
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.DockerHost;
|
||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
|
||||
import org.springframework.util.Base64Utils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
|
@ -39,7 +38,8 @@ public class DockerSpecTests {
|
|||
void asDockerConfigurationWithDefaults() {
|
||||
DockerSpec dockerSpec = new DockerSpec();
|
||||
assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull();
|
||||
assertThat(dockerSpec.asDockerConfiguration().getRegistryAuthentication()).isNull();
|
||||
assertThat(dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
|
||||
assertThat(dockerSpec.asDockerConfiguration().getPublishRegistryAuthentication()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -53,7 +53,8 @@ public class DockerSpecTests {
|
|||
assertThat(host.getAddress()).isEqualTo("docker.example.com");
|
||||
assertThat(host.isSecure()).isEqualTo(true);
|
||||
assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert");
|
||||
assertThat(dockerSpec.asDockerConfiguration().getRegistryAuthentication()).isNull();
|
||||
assertThat(dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
|
||||
assertThat(dockerSpec.asDockerConfiguration().getPublishRegistryAuthentication()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -65,59 +66,71 @@ public class DockerSpecTests {
|
|||
assertThat(host.getAddress()).isEqualTo("docker.example.com");
|
||||
assertThat(host.isSecure()).isEqualTo(false);
|
||||
assertThat(host.getCertificatePath()).isNull();
|
||||
assertThat(dockerSpec.asDockerConfiguration().getRegistryAuthentication()).isNull();
|
||||
assertThat(dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
|
||||
assertThat(dockerSpec.asDockerConfiguration().getPublishRegistryAuthentication()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void asDockerConfigurationWithUserAuth() {
|
||||
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
|
||||
dockerRegistry.setUsername("user");
|
||||
dockerRegistry.setPassword("secret");
|
||||
dockerRegistry.setUrl("https://docker.example.com");
|
||||
dockerRegistry.setEmail("docker@example.com");
|
||||
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
|
||||
DockerSpec dockerSpec = new DockerSpec(
|
||||
new DockerSpec.DockerRegistrySpec("user1", "secret1", "https://docker1.example.com",
|
||||
"docker1@example.com"),
|
||||
new DockerSpec.DockerRegistrySpec("user2", "secret2", "https://docker2.example.com",
|
||||
"docker2@example.com"));
|
||||
DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration();
|
||||
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
|
||||
assertThat(registryAuthentication).isNotNull();
|
||||
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
|
||||
.contains("\"username\" : \"user\"").contains("\"password\" : \"secret\"")
|
||||
.contains("\"email\" : \"docker@example.com\"")
|
||||
.contains("\"serveraddress\" : \"https://docker.example.com\"");
|
||||
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
|
||||
.contains("\"username\" : \"user1\"").contains("\"password\" : \"secret1\"")
|
||||
.contains("\"email\" : \"docker1@example.com\"")
|
||||
.contains("\"serveraddress\" : \"https://docker1.example.com\"");
|
||||
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
|
||||
.contains("\"username\" : \"user2\"").contains("\"password\" : \"secret2\"")
|
||||
.contains("\"email\" : \"docker2@example.com\"")
|
||||
.contains("\"serveraddress\" : \"https://docker2.example.com\"");
|
||||
assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void asDockerConfigurationWithIncompleteUserAuthFails() {
|
||||
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
|
||||
dockerRegistry.setUsername("user");
|
||||
dockerRegistry.setUrl("https://docker.example.com");
|
||||
dockerRegistry.setEmail("docker@example.com");
|
||||
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
|
||||
void asDockerConfigurationWithIncompleteBuilderUserAuthFails() {
|
||||
DockerSpec.DockerRegistrySpec builderRegistry = new DockerSpec.DockerRegistrySpec("user", null,
|
||||
"https://docker.example.com", "docker@example.com");
|
||||
DockerSpec dockerSpec = new DockerSpec(builderRegistry, null);
|
||||
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
|
||||
void asDockerConfigurationWithTokenAuth() {
|
||||
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
|
||||
dockerRegistry.setToken("token");
|
||||
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
|
||||
DockerSpec dockerSpec = new DockerSpec(new DockerSpec.DockerRegistrySpec("token1"),
|
||||
new DockerSpec.DockerRegistrySpec("token2"));
|
||||
DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration();
|
||||
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
|
||||
assertThat(registryAuthentication).isNotNull();
|
||||
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
|
||||
.contains("\"identitytoken\" : \"token\"");
|
||||
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
|
||||
.contains("\"identitytoken\" : \"token1\"");
|
||||
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
|
||||
.contains("\"identitytoken\" : \"token2\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void asDockerConfigurationWithUserAndTokenAuthFails() {
|
||||
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
|
||||
dockerRegistry.setUsername("user");
|
||||
dockerRegistry.setPassword("secret");
|
||||
dockerRegistry.setToken("token");
|
||||
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
|
||||
DockerSpec.DockerRegistrySpec builderRegistry = new DockerSpec.DockerRegistrySpec();
|
||||
builderRegistry.setUsername("user");
|
||||
builderRegistry.setPassword("secret");
|
||||
builderRegistry.setToken("token");
|
||||
DockerSpec dockerSpec = new DockerSpec(builderRegistry, null);
|
||||
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.junit.jupiter:junit-jupiter")
|
||||
intTestImplementation("org.testcontainers:testcontainers")
|
||||
intTestImplementation("org.testcontainers:junit-jupiter")
|
||||
|
||||
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]]
|
||||
=== 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.
|
||||
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.
|
||||
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.
|
||||
|
||||
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
|
||||
|
|
@ -156,6 +159,11 @@ Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`.
|
|||
| Enables verbose logging of builder operations.
|
||||
|
|
||||
| `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.
|
||||
|
|
@ -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]]
|
||||
==== 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:
|
||||
|
|
@ -329,7 +379,7 @@ If you need the plugin to communicate with the Docker daemon using a remote conn
|
|||
</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"]
|
||||
----
|
||||
|
|
@ -342,12 +392,12 @@ If the builder or run image are stored in a private Docker registry that support
|
|||
<version>{gradle-project-version}</version>
|
||||
<configuration>
|
||||
<docker>
|
||||
<registry>
|
||||
<builderRegistry>
|
||||
<username>user</username>
|
||||
<password>secret</password>
|
||||
<url>https://docker.example.com/v1/</url>
|
||||
<email>user@example.com</email>
|
||||
</registry>
|
||||
</builderRegistry>
|
||||
</docker>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
|
@ -356,7 +406,7 @@ If the builder or run image are stored in a private Docker registry that support
|
|||
</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"]
|
||||
----
|
||||
|
|
@ -369,9 +419,9 @@ If the builder or run image is stored in a private Docker registry that supports
|
|||
<version>{gradle-project-version}</version>
|
||||
<configuration>
|
||||
<docker>
|
||||
<registry>
|
||||
<builderRegistry>
|
||||
<token>9cbaf023786cd7...</token>
|
||||
</registry>
|
||||
</builderRegistry>
|
||||
</docker>
|
||||
</configuration>
|
||||
</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
|
||||
void failsWhenBuilderFails(MavenBuild mavenBuild) {
|
||||
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;
|
||||
|
||||
/**
|
||||
* Image configuration, with `builder`, `runImage`, `name`, `env`, `cleanCache` and
|
||||
* `verboseLogging` options.
|
||||
* Image configuration, with `builder`, `runImage`, `name`, `env`, `cleanCache`,
|
||||
* `verboseLogging`, and `publish` options.
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@Parameter
|
||||
|
|
@ -136,6 +136,12 @@ public class BuildImageMojo extends AbstractPackagerMojo {
|
|||
@Parameter(property = "spring-boot.build-image.pullPolicy", readonly = true)
|
||||
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.
|
||||
* @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);
|
||||
Image image = (this.image != null) ? this.image : new Image();
|
||||
if (image.name == null && this.imageName != null) {
|
||||
|
|
@ -185,9 +191,20 @@ public class BuildImageMojo extends AbstractPackagerMojo {
|
|||
if (image.pullPolicy == null && this.pullPolicy != null) {
|
||||
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));
|
||||
}
|
||||
|
||||
private boolean publishRegistryNotConfigured() {
|
||||
return this.docker == null || this.docker.getPublishRegistry() == null
|
||||
|| this.docker.getPublishRegistry().isEmpty();
|
||||
}
|
||||
|
||||
private TarArchive getApplicationContent(Owner owner, Libraries libraries) {
|
||||
ImagePackager packager = getConfiguredPackager(() -> new ImagePackager(getJarFile()));
|
||||
return new PackagedTarArchive(owner, libraries, packager);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,9 @@ public class Docker {
|
|||
|
||||
private String certPath;
|
||||
|
||||
private DockerRegistry registry;
|
||||
private DockerRegistry builderRegistry;
|
||||
|
||||
private DockerRegistry publishRegistry;
|
||||
|
||||
public String getHost() {
|
||||
return this.host;
|
||||
|
|
@ -59,12 +61,30 @@ public class Docker {
|
|||
this.certPath = certPath;
|
||||
}
|
||||
|
||||
DockerRegistry getBuilderRegistry() {
|
||||
return this.builderRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link DockerRegistry} that configures registry authentication.
|
||||
* @param registry the registry configuration
|
||||
* Sets the {@link DockerRegistry} that configures authentication to the builder
|
||||
* registry.
|
||||
* @param builderRegistry the registry configuration
|
||||
*/
|
||||
public void setRegistry(DockerRegistry registry) {
|
||||
this.registry = registry;
|
||||
public void setBuilderRegistry(DockerRegistry builderRegistry) {
|
||||
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 dockerConfiguration = new DockerConfiguration();
|
||||
dockerConfiguration = customizeHost(dockerConfiguration);
|
||||
dockerConfiguration = customizeAuthentication(dockerConfiguration);
|
||||
dockerConfiguration = customizeBuilderAuthentication(dockerConfiguration);
|
||||
dockerConfiguration = customizePublishAuthentication(dockerConfiguration);
|
||||
return dockerConfiguration;
|
||||
}
|
||||
|
||||
|
|
@ -87,19 +108,34 @@ public class Docker {
|
|||
return dockerConfiguration;
|
||||
}
|
||||
|
||||
private DockerConfiguration customizeAuthentication(DockerConfiguration dockerConfiguration) {
|
||||
if (this.registry == null || this.registry.isEmpty()) {
|
||||
private DockerConfiguration customizeBuilderAuthentication(DockerConfiguration dockerConfiguration) {
|
||||
if (this.builderRegistry == null || this.builderRegistry.isEmpty()) {
|
||||
return dockerConfiguration;
|
||||
}
|
||||
if (this.registry.hasTokenAuth() && !this.registry.hasUserAuth()) {
|
||||
return dockerConfiguration.withRegistryTokenAuthentication(this.registry.getToken());
|
||||
if (this.builderRegistry.hasTokenAuth() && !this.builderRegistry.hasUserAuth()) {
|
||||
return dockerConfiguration.withBuilderRegistryTokenAuthentication(this.builderRegistry.getToken());
|
||||
}
|
||||
if (this.registry.hasUserAuth() && !this.registry.hasTokenAuth()) {
|
||||
return dockerConfiguration.withRegistryUserAuthentication(this.registry.getUsername(),
|
||||
this.registry.getPassword(), this.registry.getUrl(), this.registry.getEmail());
|
||||
if (this.builderRegistry.hasUserAuth() && !this.builderRegistry.hasTokenAuth()) {
|
||||
return dockerConfiguration.withBuilderRegistryUserAuthentication(this.builderRegistry.getUsername(),
|
||||
this.builderRegistry.getPassword(), this.builderRegistry.getUrl(), this.builderRegistry.getEmail());
|
||||
}
|
||||
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;
|
||||
|
||||
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() {
|
||||
return this.username;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,11 @@ public class Image {
|
|||
*/
|
||||
PullPolicy pullPolicy;
|
||||
|
||||
/**
|
||||
* If the built image should be pushed to a registry.
|
||||
*/
|
||||
Boolean publish;
|
||||
|
||||
void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
|
@ -89,6 +94,10 @@ public class Image {
|
|||
this.pullPolicy = pullPolicy;
|
||||
}
|
||||
|
||||
public void setPublish(Boolean publish) {
|
||||
this.publish = publish;
|
||||
}
|
||||
|
||||
BuildRequest getBuildRequest(Artifact artifact, Function<Owner, TarArchive> applicationContent) {
|
||||
return customize(BuildRequest.of(getOrDeduceName(artifact), applicationContent));
|
||||
}
|
||||
|
|
@ -116,6 +125,9 @@ public class Image {
|
|||
if (this.pullPolicy != null) {
|
||||
request = request.withPullPolicy(this.pullPolicy);
|
||||
}
|
||||
if (this.publish != null) {
|
||||
request = request.withPublish(this.publish);
|
||||
}
|
||||
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.DockerHost;
|
||||
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
|
||||
import org.springframework.util.Base64Utils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
|
@ -38,7 +37,8 @@ public class DockerTests {
|
|||
void asDockerConfigurationWithDefaults() {
|
||||
Docker docker = new Docker();
|
||||
assertThat(docker.asDockerConfiguration().getHost()).isNull();
|
||||
assertThat(docker.asDockerConfiguration().getRegistryAuthentication()).isNull();
|
||||
assertThat(docker.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
|
||||
assertThat(docker.asDockerConfiguration().getPublishRegistryAuthentication()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -52,50 +52,56 @@ public class DockerTests {
|
|||
assertThat(host.getAddress()).isEqualTo("docker.example.com");
|
||||
assertThat(host.isSecure()).isEqualTo(true);
|
||||
assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert");
|
||||
assertThat(docker.asDockerConfiguration().getRegistryAuthentication()).isNull();
|
||||
assertThat(docker.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
|
||||
assertThat(docker.asDockerConfiguration().getPublishRegistryAuthentication()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
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.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();
|
||||
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
|
||||
assertThat(registryAuthentication).isNotNull();
|
||||
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
|
||||
.contains("\"username\" : \"user\"").contains("\"password\" : \"secret\"")
|
||||
.contains("\"email\" : \"docker@example.com\"")
|
||||
.contains("\"serveraddress\" : \"https://docker.example.com\"");
|
||||
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
|
||||
.contains("\"username\" : \"user1\"").contains("\"password\" : \"secret1\"")
|
||||
.contains("\"email\" : \"docker1@example.com\"")
|
||||
.contains("\"serveraddress\" : \"https://docker1.example.com\"");
|
||||
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
|
||||
.contains("\"username\" : \"user2\"").contains("\"password\" : \"secret2\"")
|
||||
.contains("\"email\" : \"docker2@example.com\"")
|
||||
.contains("\"serveraddress\" : \"https://docker2.example.com\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void asDockerConfigurationWithIncompleteUserAuthFails() {
|
||||
Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry();
|
||||
dockerRegistry.setUsername("user");
|
||||
dockerRegistry.setUrl("https://docker.example.com");
|
||||
dockerRegistry.setEmail("docker@example.com");
|
||||
void asDockerConfigurationWithIncompleteBuilderUserAuthFails() {
|
||||
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)
|
||||
.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
|
||||
void asDockerConfigurationWithTokenAuth() {
|
||||
Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry();
|
||||
dockerRegistry.setToken("token");
|
||||
Docker docker = new Docker();
|
||||
docker.setRegistry(dockerRegistry);
|
||||
docker.setBuilderRegistry(new Docker.DockerRegistry("token1"));
|
||||
docker.setPublishRegistry(new Docker.DockerRegistry("token2"));
|
||||
DockerConfiguration dockerConfiguration = docker.asDockerConfiguration();
|
||||
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
|
||||
assertThat(registryAuthentication).isNotNull();
|
||||
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
|
||||
.contains("\"identitytoken\" : \"token\"");
|
||||
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
|
||||
.contains("\"identitytoken\" : \"token1\"");
|
||||
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
|
||||
.contains("\"identitytoken\" : \"token2\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -105,9 +111,13 @@ public class DockerTests {
|
|||
dockerRegistry.setPassword("secret");
|
||||
dockerRegistry.setToken("token");
|
||||
Docker docker = new Docker();
|
||||
docker.setRegistry(dockerRegistry);
|
||||
docker.setBuilderRegistry(dockerRegistry);
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBuildRequestWhenHasPublishUsesPublish() {
|
||||
Image image = new Image();
|
||||
image.publish = true;
|
||||
BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent());
|
||||
assertThat(request.isPublish()).isTrue();
|
||||
}
|
||||
|
||||
private Artifact createArtifact() {
|
||||
return new DefaultArtifact("com.example", "my-app", VersionRange.createFromVersion("0.0.1-SNAPSHOT"), "compile",
|
||||
"jar", null, new DefaultArtifactHandler());
|
||||
|
|
|
|||
Loading…
Reference in New Issue