diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index c461280df68..0bf2e33213d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -26,7 +26,8 @@ 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.configuration.DockerConnectionConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.Binding; @@ -54,7 +55,7 @@ public class Builder { private final DockerApi docker; - private final DockerConfiguration dockerConfiguration; + private final BuilderDockerConfiguration dockerConfiguration; /** * Create a new builder instance. @@ -67,8 +68,22 @@ public class Builder { * Create a new builder instance. * @param dockerConfiguration the docker configuration * @since 2.4.0 + * @deprecated since 3.5.0 for removal in 4.0.0 in favor of + * {@link #Builder(BuilderDockerConfiguration)} */ - public Builder(DockerConfiguration dockerConfiguration) { + @Deprecated(since = "3.5.0", forRemoval = true) + @SuppressWarnings("removal") + public Builder( + org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration dockerConfiguration) { + this(BuildLog.toSystemOut(), dockerConfiguration); + } + + /** + * Create a new builder instance. + * @param dockerConfiguration the docker configuration + * @since 3.5.0 + */ + public Builder(BuilderDockerConfiguration dockerConfiguration) { this(BuildLog.toSystemOut(), dockerConfiguration); } @@ -85,17 +100,45 @@ public class Builder { * @param log a logger used to record output * @param dockerConfiguration the docker configuration * @since 2.4.0 + * @deprecated since 3.5.0 for removal in 4.0.0 in favor of + * {@link #Builder(BuildLog, BuilderDockerConfiguration)} */ - public Builder(BuildLog log, DockerConfiguration dockerConfiguration) { - this(log, new DockerApi((dockerConfiguration != null) ? dockerConfiguration.getHost() : null, + @Deprecated(since = "3.5.0", forRemoval = true) + @SuppressWarnings("removal") + public Builder(BuildLog log, + org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration dockerConfiguration) { + this(log, adaptDeprecatedConfiguration(dockerConfiguration)); + } + + @SuppressWarnings("removal") + private static BuilderDockerConfiguration adaptDeprecatedConfiguration( + org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration configuration) { + if (configuration == null) { + return null; + } + DockerConnectionConfiguration connection = org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration + .asConnectionConfiguration(configuration.getHost()); + return new BuilderDockerConfiguration(connection, configuration.isBindHostToBuilder(), + configuration.getBuilderRegistryAuthentication(), configuration.getPublishRegistryAuthentication()); + } + + /** + * Create a new builder instance. + * @param log a logger used to record output + * @param dockerConfiguration the docker configuration + * @since 3.5.0 + */ + public Builder(BuildLog log, BuilderDockerConfiguration dockerConfiguration) { + this(log, new DockerApi((dockerConfiguration != null) ? dockerConfiguration.connection() : null, BuildLogAdapter.get(log)), dockerConfiguration); } - Builder(BuildLog log, DockerApi docker, DockerConfiguration dockerConfiguration) { + Builder(BuildLog log, DockerApi docker, BuilderDockerConfiguration dockerConfiguration) { Assert.notNull(log, "'log' must not be null"); this.log = log; this.docker = docker; - this.dockerConfiguration = dockerConfiguration; + this.dockerConfiguration = (dockerConfiguration != null) ? dockerConfiguration + : new BuilderDockerConfiguration(); } public void build(BuildRequest request) throws DockerEngineException, IOException { @@ -104,8 +147,8 @@ public class Builder { validateBindings(request.getBindings()); String domain = request.getBuilder().getDomain(); PullPolicy pullPolicy = request.getPullPolicy(); - ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy, - request.getImagePlatform()); + ImageFetcher imageFetcher = new ImageFetcher(domain, this.dockerConfiguration.builderRegistryAuthentication(), + pullPolicy, request.getImagePlatform()); Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder()); BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage); request = withRunImageIfNeeded(request, builderMetadata); @@ -182,8 +225,8 @@ public class Builder { } private ResolvedDockerHost getDockerHost() { - boolean bindHostToBuilder = this.dockerConfiguration != null && this.dockerConfiguration.isBindHostToBuilder(); - return (bindHostToBuilder) ? ResolvedDockerHost.from(this.dockerConfiguration.getHost()) : null; + boolean bindToBuilder = this.dockerConfiguration.bindHostToBuilder(); + return (bindToBuilder) ? ResolvedDockerHost.from(this.dockerConfiguration.connection()) : null; } private void tagImage(ImageReference sourceReference, List tags) throws IOException { @@ -203,18 +246,13 @@ public class Builder { private void pushImage(ImageReference reference) throws IOException { Consumer progressConsumer = this.log.pushingImage(reference); TotalProgressPushListener listener = new TotalProgressPushListener(progressConsumer); - this.docker.image().push(reference, listener, getPublishAuthHeader()); + String authHeader = authHeader(this.dockerConfiguration.publishRegistryAuthentication()); + this.docker.image().push(reference, listener, authHeader); 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 static String authHeader(DockerRegistryAuthentication authentication) { + return (authentication != null) ? authentication.getAuthHeader() : null; } /** @@ -224,15 +262,16 @@ public class Builder { private final String domain; - private final String authHeader; + private final DockerRegistryAuthentication registryAuthentication; private final PullPolicy pullPolicy; private ImagePlatform defaultPlatform; - ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy, ImagePlatform platform) { + ImageFetcher(String domain, DockerRegistryAuthentication registryAuthentication, PullPolicy pullPolicy, + ImagePlatform platform) { this.domain = domain; - this.authHeader = authHeader; + this.registryAuthentication = registryAuthentication; this.pullPolicy = pullPolicy; this.defaultPlatform = platform; } @@ -240,7 +279,8 @@ public class Builder { Image fetchImage(ImageType type, ImageReference reference) throws IOException { Assert.notNull(type, "'type' must not be null"); Assert.notNull(reference, "'reference' must not be null"); - Assert.state(this.authHeader == null || reference.getDomain().equals(this.domain), + String authHeader = authHeader(this.registryAuthentication); + Assert.state(authHeader == null || reference.getDomain().equals(this.domain), () -> String.format("%s '%s' must be pulled from the '%s' authenticated registry", StringUtils.capitalize(type.getDescription()), reference, this.domain)); if (this.pullPolicy == PullPolicy.ALWAYS) { @@ -260,7 +300,8 @@ public class Builder { private Image pullImage(ImageReference reference, ImageType imageType) throws IOException { TotalProgressPullListener listener = new TotalProgressPullListener( Builder.this.log.pullingImage(reference, this.defaultPlatform, imageType)); - Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, this.authHeader); + String authHeader = authHeader(this.registryAuthentication); + Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, authHeader); Builder.this.log.pulledImage(image, imageType); if (this.defaultPlatform == null) { this.defaultPlatform = ImagePlatform.from(image); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderDockerConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderDockerConfiguration.java new file mode 100644 index 00000000000..9138ea68a80 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuilderDockerConfiguration.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2025 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.build; + +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConnectionConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication; + +/** + * {@link Builder} configuration options for Docker. + * + * @param connection the Docker host configuration + * @param bindHostToBuilder if the host resolved from the connection should be bound to + * the builder + * @param builderRegistryAuthentication the builder {@link DockerRegistryAuthentication} + * @param publishRegistryAuthentication the publish {@link DockerRegistryAuthentication} + * @author Phillip Webb + * @author Wei Jiang + * @author Scott Frederick + * @since 3.5.0 + */ +public record BuilderDockerConfiguration(DockerConnectionConfiguration connection, boolean bindHostToBuilder, + DockerRegistryAuthentication builderRegistryAuthentication, + DockerRegistryAuthentication publishRegistryAuthentication) { + + public BuilderDockerConfiguration() { + this(null, false, null, null); + } + + public BuilderDockerConfiguration withContext(String context) { + return withConnection(new DockerConnectionConfiguration.Context(context)); + } + + public BuilderDockerConfiguration withHost(String address, boolean secure, String certificatePath) { + return withConnection(new DockerConnectionConfiguration.Host(address, secure, certificatePath)); + } + + private BuilderDockerConfiguration withConnection(DockerConnectionConfiguration hostConfiguration) { + return new BuilderDockerConfiguration(hostConfiguration, this.bindHostToBuilder, + this.builderRegistryAuthentication, this.publishRegistryAuthentication); + } + + public BuilderDockerConfiguration withBindHostToBuilder(boolean bindHostToBuilder) { + return new BuilderDockerConfiguration(this.connection, bindHostToBuilder, this.builderRegistryAuthentication, + this.publishRegistryAuthentication); + } + + public BuilderDockerConfiguration withBuilderRegistryAuthentication( + DockerRegistryAuthentication builderRegistryAuthentication) { + return new BuilderDockerConfiguration(this.connection, this.bindHostToBuilder, builderRegistryAuthentication, + this.publishRegistryAuthentication); + + } + + public BuilderDockerConfiguration withPublishRegistryAuthentication( + DockerRegistryAuthentication publishRegistryAuthentication) { + return new BuilderDockerConfiguration(this.connection, this.bindHostToBuilder, + this.builderRegistryAuthentication, publishRegistryAuthentication); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java index d3344654b0e..ed8b88decca 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java @@ -28,7 +28,7 @@ import java.util.Objects; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.net.URIBuilder; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConnectionConfiguration; import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport; import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response; import org.springframework.boot.buildpack.platform.docker.type.ApiVersion; @@ -87,26 +87,32 @@ public class DockerApi { * Create a new {@link DockerApi} instance. */ public DockerApi() { - this(null); + this(HttpTransport.create((DockerConnectionConfiguration) null), DockerLog.toSystemOut()); } /** * Create a new {@link DockerApi} instance. * @param dockerHost the Docker daemon host information * @since 2.4.0 + * @deprecated since 3.5.0 for removal in 4.0.0 in favor of + * {@link #DockerApi(DockerConnectionConfiguration, DockerLog)} */ - public DockerApi(DockerHostConfiguration dockerHost) { - this(dockerHost, DockerLog.toSystemOut()); + @Deprecated(since = "3.5.0", forRemoval = true) + @SuppressWarnings("removal") + public DockerApi( + org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration dockerHost) { + this(org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration + .asConnectionConfiguration(dockerHost), DockerLog.toSystemOut()); } /** * Create a new {@link DockerApi} instance. - * @param dockerHost the Docker daemon host information + * @param connectionConfiguration the connection configuration to use * @param log a logger used to record output * @since 3.5.0 */ - public DockerApi(DockerHostConfiguration dockerHost, DockerLog log) { - this(HttpTransport.create(dockerHost), log); + public DockerApi(DockerConnectionConfiguration connectionConfiguration, DockerLog log) { + this(HttpTransport.create(connectionConfiguration), log); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfiguration.java index e04f4c9cb10..bf6f1ebd8a7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfiguration.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.boot.buildpack.platform.docker.configuration; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Docker configuration options. @@ -24,7 +25,11 @@ import org.springframework.util.Assert; * @author Wei Jiang * @author Scott Frederick * @since 2.4.0 + * @deprecated since 3.5.0 for removal in 4.0.0 in favor of + * {@link org.springframework.boot.buildpack.platform.build.BuilderDockerConfiguration}. */ +@Deprecated(since = "3.5.0", forRemoval = true) +@SuppressWarnings("removal") public final class DockerConfiguration { private final DockerHostConfiguration host; @@ -113,6 +118,13 @@ public final class DockerConfiguration { new DockerRegistryUserAuthentication("", "", "", ""), this.bindHostToBuilder); } + /** + * Docker host configuration. + * + * @deprecated since 3.5.0 for removal in 4.0.0 in favor of + * {@link DockerHostConfiguration} + */ + @Deprecated(since = "3.5.0", forRemoval = true) public static class DockerHostConfiguration { private final String address; @@ -158,6 +170,24 @@ public final class DockerConfiguration { return new DockerHostConfiguration(null, context, false, null); } + /** + * Adapts a {@link DockerHostConfiguration} to a + * {@link DockerConnectionConfiguration}. + * @param configuration the configuration to adapt + * @return the adapted configuration + * @since 3.5.0 + */ + public static DockerConnectionConfiguration asConnectionConfiguration(DockerHostConfiguration configuration) { + if (configuration != null && StringUtils.hasLength(configuration.context)) { + return new DockerConnectionConfiguration.Context(configuration.context); + } + if (configuration != null && StringUtils.hasLength(configuration.address)) { + return new DockerConnectionConfiguration.Host(configuration.address, configuration.secure, + configuration.certificatePath); + } + return null; + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConnectionConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConnectionConfiguration.java new file mode 100644 index 00000000000..c1b431d8b8e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConnectionConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2025 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.configuration; + +import org.springframework.util.Assert; + +/** + * Configuration for how to connect to Docker. + * + * @author Phillip Webb + * @since 3.5.0 + */ +public sealed interface DockerConnectionConfiguration { + + /** + * Connect to specific host. + * + * @param address the host address + * @param secure if connection is secure + * @param certificatePath a path to the certificate used for secure connections + */ + record Host(String address, boolean secure, String certificatePath) implements DockerConnectionConfiguration { + + public Host(String address) { + this(address, false, null); + } + + public Host { + Assert.hasLength(address, "'address' must not be empty"); + } + + } + + /** + * Connect using a specific context reference. + * + * @param context a reference to the Docker context + */ + record Context(String context) implements DockerConnectionConfiguration { + + public Context { + Assert.hasLength(context, "'context' must not be empty"); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java index 3df4b4fadcb..1e4f4a1dbf5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2025 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. @@ -24,10 +24,43 @@ package org.springframework.boot.buildpack.platform.docker.configuration; */ public interface DockerRegistryAuthentication { + /** + * An empty {@link #user(String, String, String, String)} authentication. + * @since 3.5.0 + */ + DockerRegistryAuthentication EMPTY_USER = DockerRegistryAuthentication.user("", "", "", ""); + /** * Returns the auth header that should be used for docker authentication. * @return the auth header */ String getAuthHeader(); + /** + * Factory method to that returns a new {@link DockerRegistryAuthentication} instance + * that uses a header generated by base64 encoding a JSON payload created from the + * given parameters. + * @param identityToken the identity token JSON field + * @return a new {@link DockerRegistryAuthentication} instance + * @since 3.5.0 + */ + static DockerRegistryAuthentication token(String identityToken) { + return new DockerRegistryTokenAuthentication(identityToken); + } + + /** + * Factory method to that returns a new {@link DockerRegistryAuthentication} instance + * that uses a header generated by base64 encoding a JSON payload created from the + * given parameters. + * @param username the username JSON field + * @param password the password JSON field + * @param serverAddress the server address JSON field + * @param email the email JSON field + * @return a new {@link DockerRegistryAuthentication} instance + * @since 3.5.0 + */ + static DockerRegistryAuthentication user(String username, String password, String serverAddress, String email) { + return new DockerRegistryUserAuthentication(username, password, serverAddress, email); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHost.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHost.java index ddaab19933a..cd556c91804 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHost.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHost.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -21,7 +21,6 @@ import java.nio.file.Paths; import com.sun.jna.Platform; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.DockerContext; import org.springframework.boot.buildpack.platform.system.Environment; @@ -77,30 +76,49 @@ public class ResolvedDockerHost extends DockerHost { } } - public static ResolvedDockerHost from(DockerHostConfiguration dockerHost) { - return from(Environment.SYSTEM, dockerHost); + /** + * Create a new {@link ResolvedDockerHost} from the given host configuration. + * @param dockerHostConfiguration the host configuration or {@code null} + * @return the resolved docker host + * @deprecated since 3.5.0 for removal in 4.0.0 in favor of + * {@link #from(DockerConnectionConfiguration)} + */ + @Deprecated(since = "3.5.0", forRemoval = true) + @SuppressWarnings("removal") + public static ResolvedDockerHost from(DockerConfiguration.DockerHostConfiguration dockerHostConfiguration) { + return from(Environment.SYSTEM, + DockerConfiguration.DockerHostConfiguration.asConnectionConfiguration(dockerHostConfiguration)); } - static ResolvedDockerHost from(Environment environment, DockerHostConfiguration dockerHost) { - DockerConfigurationMetadata config = DockerConfigurationMetadata.from(environment); + /** + * Create a new {@link ResolvedDockerHost} from the given host configuration. + * @param connectionConfiguration the host configuration or {@code null} + * @return the resolved docker host + */ + public static ResolvedDockerHost from(DockerConnectionConfiguration connectionConfiguration) { + return from(Environment.SYSTEM, connectionConfiguration); + } + + static ResolvedDockerHost from(Environment environment, DockerConnectionConfiguration connectionConfiguration) { + DockerConfigurationMetadata environmentConfiguration = DockerConfigurationMetadata.from(environment); if (environment.get(DOCKER_CONTEXT) != null) { - DockerContext context = config.forContext(environment.get(DOCKER_CONTEXT)); + DockerContext context = environmentConfiguration.forContext(environment.get(DOCKER_CONTEXT)); return new ResolvedDockerHost(context.getDockerHost(), context.isTlsVerify(), context.getTlsPath()); } - if (dockerHost != null && dockerHost.getContext() != null) { - DockerContext context = config.forContext(dockerHost.getContext()); + if (connectionConfiguration instanceof DockerConnectionConfiguration.Context contextConfiguration) { + DockerContext context = environmentConfiguration.forContext(contextConfiguration.context()); return new ResolvedDockerHost(context.getDockerHost(), context.isTlsVerify(), context.getTlsPath()); } if (environment.get(DOCKER_HOST) != null) { return new ResolvedDockerHost(environment.get(DOCKER_HOST), isTrue(environment.get(DOCKER_TLS_VERIFY)), environment.get(DOCKER_CERT_PATH)); } - if (dockerHost != null && dockerHost.getAddress() != null) { - return new ResolvedDockerHost(dockerHost.getAddress(), dockerHost.isSecure(), - dockerHost.getCertificatePath()); + if (connectionConfiguration instanceof DockerConnectionConfiguration.Host addressConfiguration) { + return new ResolvedDockerHost(addressConfiguration.address(), addressConfiguration.secure(), + addressConfiguration.certificatePath()); } - if (config.getContext().getDockerHost() != null) { - DockerContext context = config.getContext(); + if (environmentConfiguration.getContext().getDockerHost() != null) { + DockerContext context = environmentConfiguration.getContext(); return new ResolvedDockerHost(context.getDockerHost(), context.isTlsVerify(), context.getTlsPath()); } return new ResolvedDockerHost(getDefaultAddress()); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java index d18617fca57..9bb418790aa 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -24,7 +24,8 @@ import java.net.URI; import org.apache.hc.core5.http.Header; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConnectionConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; import org.springframework.boot.buildpack.platform.io.IOConsumer; @@ -103,13 +104,28 @@ public interface HttpTransport { * Create the most suitable {@link HttpTransport} based on the {@link DockerHost}. * @param dockerHost the Docker host information * @return a {@link HttpTransport} instance + * @deprecated since 3.5.0 for removal in 4.0.0 in favor of + * {@link #create(DockerConnectionConfiguration)} */ - static HttpTransport create(DockerHostConfiguration dockerHost) { + @Deprecated(since = "3.5.0", forRemoval = true) + @SuppressWarnings("removal") + static HttpTransport create(DockerConfiguration.DockerHostConfiguration dockerHost) { ResolvedDockerHost host = ResolvedDockerHost.from(dockerHost); HttpTransport remote = RemoteHttpClientTransport.createIfPossible(host); return (remote != null) ? remote : LocalHttpClientTransport.create(host); } + /** + * Create the most suitable {@link HttpTransport} based on the {@link DockerHost}. + * @param connectionConfiguration the Docker host information + * @return a {@link HttpTransport} instance + */ + static HttpTransport create(DockerConnectionConfiguration connectionConfiguration) { + ResolvedDockerHost host = ResolvedDockerHost.from(connectionConfiguration); + HttpTransport remote = RemoteHttpClientTransport.createIfPossible(host); + return (remote != null) ? remote : LocalHttpClientTransport.create(host); + } + /** * An HTTP operation response. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java index 7cba312d72e..1260fff919c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuilderTests.java @@ -32,7 +32,7 @@ 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.DockerLog; import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication; import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; @@ -49,6 +49,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.BDDMockito.given; @@ -66,6 +67,15 @@ import static org.mockito.Mockito.times; */ class BuilderTests { + private static final ImageReference PAKETO_BUILDPACKS_BUILDER = ImageReference + .of("gcr.io/paketo-buildpacks/builder"); + + private static final ImageReference LATEST_PAKETO_BUILDPACKS_BUILDER = PAKETO_BUILDPACKS_BUILDER.inTaggedForm(); + + private static final ImageReference DEFAULT_BUILDER = ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF); + + private static final ImageReference BASE_CNB = ImageReference.of("docker.io/cloudfoundry/run:base-cnb"); + @Test void createWhenLogIsNullThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> new Builder((BuildLog) null)) @@ -108,12 +118,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_REF)), isNull(), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), isNull())) + given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest(); @@ -121,11 +128,8 @@ class BuilderTests { assertThat(out.toString()).contains("Running creator"); assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); - then(docker.image()).should() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), isNull()); - then(docker.image()).should() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), isNull()); + then(docker.image()).should().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull()); + then(docker.image()).should().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull()); then(docker.image()).should().load(archive.capture(), any()); then(docker.image()).should().remove(archive.getValue().getTag(), true); then(docker.image()).shouldHaveNoMoreInteractions(); @@ -137,16 +141,14 @@ class BuilderTests { 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_REF)), isNull(), any(), - eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + DockerRegistryAuthentication builderToken = DockerRegistryAuthentication.token("builder token"); + DockerRegistryAuthentication publishToken = DockerRegistryAuthentication.token("publish token"); + BuilderDockerConfiguration dockerConfiguration = new BuilderDockerConfiguration() + .withBuilderRegistryAuthentication(builderToken) + .withPublishRegistryAuthentication(publishToken); + given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), regAuthEq(builderToken))) .willAnswer(withPulledImage(builderImage)); - given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), regAuthEq(builderToken))) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); BuildRequest request = getTestRequest().withPublish(true); @@ -154,15 +156,10 @@ class BuilderTests { assertThat(out.toString()).contains("Running creator"); assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); + then(docker.image()).should().pull(eq(DEFAULT_BUILDER), isNull(), any(), regAuthEq(builderToken)); then(docker.image()).should() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), - eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); - then(docker.image()).should() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); - then(docker.image()).should() - .push(eq(request.getName()), any(), - eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())); + .pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), regAuthEq(builderToken)); + then(docker.image()).should().push(eq(request.getName()), any(), regAuthEq(publishToken)); then(docker.image()).should().load(archive.capture(), any()); then(docker.image()).should().remove(archive.getValue().getTag(), true); then(docker.image()).shouldHaveNoMoreInteractions(); @@ -174,15 +171,14 @@ 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")), isNull(), any(), isNull())) + given(docker.image().pull(eq(LATEST_PAKETO_BUILDPACKS_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); given(docker.image() .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:latest")), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); - BuildRequest request = getTestRequest().withBuilder(ImageReference.of("gcr.io/paketo-buildpacks/builder")); + BuildRequest request = getTestRequest().withBuilder(PAKETO_BUILDPACKS_BUILDER); builder.build(request); assertThat(out.toString()).contains("Running creator"); assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); @@ -197,8 +193,7 @@ 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_REF)), isNull(), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); given(docker.image() .pull(eq(ImageReference @@ -221,15 +216,12 @@ class BuilderTests { DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image-with-empty-stack.json"); Image runImage = loadImage("run-image.json"); - given(docker.image() - .pull(eq(ImageReference.of("gcr.io/paketo-buildpacks/builder:latest")), isNull(), any(), isNull())) + given(docker.image().pull(eq(LATEST_PAKETO_BUILDPACKS_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), isNull())) + given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); - BuildRequest request = getTestRequest().withBuilder(ImageReference.of("gcr.io/paketo-buildpacks/builder")); + BuildRequest request = getTestRequest().withBuilder(PAKETO_BUILDPACKS_BUILDER); builder.build(request); assertThat(out.toString()).contains("Running creator"); assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); @@ -244,8 +236,7 @@ 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_REF)), isNull(), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); given(docker.image() .pull(eq(ImageReference.of("example.com/custom/run:latest")), eq(ImagePlatform.from(builderImage)), any(), @@ -267,17 +258,12 @@ 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_REF)), isNull(), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), isNull())) + given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); - given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)))) - .willReturn(builderImage); - given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))) - .willReturn(runImage); + given(docker.image().inspect(eq(DEFAULT_BUILDER))).willReturn(builderImage); + given(docker.image().inspect(eq(BASE_CNB))).willReturn(runImage); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.NEVER); builder.build(request); @@ -296,17 +282,12 @@ 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_REF)), isNull(), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), isNull())) + given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); - given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)))) - .willReturn(builderImage); - given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))) - .willReturn(runImage); + given(docker.image().inspect(eq(DEFAULT_BUILDER))).willReturn(builderImage); + given(docker.image().inspect(eq(BASE_CNB))).willReturn(runImage); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withPullPolicy(PullPolicy.ALWAYS); builder.build(request); @@ -325,18 +306,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_REF)), isNull(), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), isNull())) + given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); - given(docker.image().inspect(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)))) + given(docker.image().inspect(eq(DEFAULT_BUILDER))) .willThrow( new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null)) .willReturn(builderImage); - given(docker.image().inspect(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))) + given(docker.image().inspect(eq(BASE_CNB))) .willThrow( new DockerEngineException("docker://localhost/", new URI("example"), 404, "NOT FOUND", null, null)) .willReturn(runImage); @@ -358,12 +336,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_REF)), isNull(), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), isNull())) + given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withTags(ImageReference.of("my-application:1.2.3")); @@ -383,37 +358,31 @@ class BuilderTests { 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_REF)), isNull(), any(), - eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + DockerRegistryAuthentication builderToken = DockerRegistryAuthentication.token("builder token"); + DockerRegistryAuthentication publishToken = DockerRegistryAuthentication.token("publish token"); + BuilderDockerConfiguration dockerConfiguration = new BuilderDockerConfiguration() + .withBuilderRegistryAuthentication(builderToken) + .withPublishRegistryAuthentication(publishToken); + ImageReference defaultBuilderImageReference = DEFAULT_BUILDER; + given(docker.image().pull(eq(defaultBuilderImageReference), isNull(), any(), regAuthEq(builderToken))) .willAnswer(withPulledImage(builderImage)); + ImageReference baseImageReference = BASE_CNB; given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + .pull(eq(baseImageReference), eq(ImagePlatform.from(builderImage)), any(), regAuthEq(builderToken))) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); - BuildRequest request = getTestRequest().withPublish(true).withTags(ImageReference.of("my-application:1.2.3")); + ImageReference builtImageReference = ImageReference.of("my-application:1.2.3"); + BuildRequest request = getTestRequest().withPublish(true).withTags(builtImageReference); builder.build(request); assertThat(out.toString()).contains("Running creator"); assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); assertThat(out.toString()).contains("Successfully created image tag 'docker.io/library/my-application:1.2.3'"); - - then(docker.image()).should() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(), - eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); + then(docker.image()).should().pull(eq(defaultBuilderImageReference), isNull(), any(), regAuthEq(builderToken)); then(docker.image()).should() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())); - then(docker.image()).should() - .push(eq(request.getName()), any(), - eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())); - then(docker.image()).should().tag(eq(request.getName()), eq(ImageReference.of("my-application:1.2.3"))); - then(docker.image()).should() - .push(eq(ImageReference.of("my-application:1.2.3")), any(), - eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())); + .pull(eq(baseImageReference), eq(ImagePlatform.from(builderImage)), any(), regAuthEq(builderToken)); + then(docker.image()).should().push(eq(request.getName()), any(), regAuthEq(publishToken)); + then(docker.image()).should().tag(eq(request.getName()), eq(builtImageReference)); + then(docker.image()).should().push(eq(builtImageReference), any(), regAuthEq(publishToken)); ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); then(docker.image()).should().load(archive.capture(), any()); then(docker.image()).should().remove(archive.getValue().getTag(), true); @@ -427,22 +396,17 @@ class BuilderTests { DockerApi docker = mockDockerApi(platform); Image builderImage = loadImage("image-with-platform.json"); Image runImage = loadImage("run-image-with-platform.json"); - given(docker.image() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), eq(platform), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), eq(platform), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(platform), any(), isNull())) - .willAnswer(withPulledImage(runImage)); + given(docker.image().pull(eq(BASE_CNB), eq(platform), any(), isNull())).willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withImagePlatform("linux/arm64/v1"); builder.build(request); assertThat(out.toString()).contains("Running creator"); assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); ArgumentCaptor archive = ArgumentCaptor.forClass(ImageArchive.class); - then(docker.image()).should() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), eq(platform), any(), isNull()); - then(docker.image()).should() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(platform), any(), isNull()); + then(docker.image()).should().pull(eq(DEFAULT_BUILDER), eq(platform), any(), isNull()); + then(docker.image()).should().pull(eq(BASE_CNB), eq(platform), any(), isNull()); then(docker.image()).should().load(archive.capture(), any()); then(docker.image()).should().remove(archive.getValue().getTag(), true); then(docker.image()).shouldHaveNoMoreInteractions(); @@ -454,12 +418,9 @@ 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_REF)), isNull(), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), isNull())) + given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest(); @@ -474,12 +435,9 @@ 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_REF)), isNull(), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), isNull())) + given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest(); @@ -492,11 +450,11 @@ class BuilderTests { TestPrintStream out = new TestPrintStream(); DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image-with-run-image-different-registry.json"); - DockerConfiguration dockerConfiguration = new DockerConfiguration() - .withBuilderRegistryTokenAuthentication("builder token"); - given(docker.image() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(), - eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + DockerRegistryAuthentication builderToken = DockerRegistryAuthentication.token("builder token"); + BuilderDockerConfiguration dockerConfiguration = new BuilderDockerConfiguration() + .withBuilderRegistryAuthentication(builderToken); + ImageReference builderImageReference = DEFAULT_BUILDER; + given(docker.image().pull(eq(builderImageReference), any(), any(), regAuthEq(builderToken))) .willAnswer(withPulledImage(builderImage)); Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); BuildRequest request = getTestRequest(); @@ -510,11 +468,10 @@ class BuilderTests { TestPrintStream out = new TestPrintStream(); DockerApi docker = mockDockerApi(); Image builderImage = loadImage("image.json"); - DockerConfiguration dockerConfiguration = new DockerConfiguration() - .withBuilderRegistryTokenAuthentication("builder token"); - given(docker.image() - .pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(), - eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))) + DockerRegistryAuthentication builderToken = DockerRegistryAuthentication.token("builder token"); + BuilderDockerConfiguration dockerConfiguration = new BuilderDockerConfiguration() + .withBuilderRegistryAuthentication(builderToken); + given(docker.image().pull(eq(DEFAULT_BUILDER), any(), any(), regAuthEq(builderToken))) .willAnswer(withPulledImage(builderImage)); Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration); BuildRequest request = getTestRequest().withRunImage(ImageReference.of("example.com/custom/run:latest")); @@ -529,11 +486,9 @@ 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_REF)), any(), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), any(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), any(), isNull())) - .willAnswer(withPulledImage(runImage)); + given(docker.image().pull(eq(BASE_CNB), any(), any(), isNull())).willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildpackReference reference = BuildpackReference.of("urn:cnb:builder:example/buildpack@1.2.3"); BuildRequest request = getTestRequest().withBuildpacks(reference); @@ -548,12 +503,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_REF)), isNull(), any(), isNull())) + given(docker.image().pull(eq(DEFAULT_BUILDER), isNull(), any(), isNull())) .willAnswer(withPulledImage(builderImage)); - given(docker.image() - .pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)), - any(), isNull())) + given(docker.image().pull(eq(BASE_CNB), eq(ImagePlatform.from(builderImage)), any(), isNull())) .willAnswer(withPulledImage(runImage)); Builder builder = new Builder(BuildLog.to(out), docker, null); BuildRequest request = getTestRequest().withBindings(Binding.from("/host", "/cnb")); @@ -611,7 +563,10 @@ class BuilderTests { listener.onFinish(); return image; }; + } + private static String regAuthEq(DockerRegistryAuthentication authentication) { + return argThat(authentication.getAuthHeader()::equals); } static class TestPrintStream extends PrintStream { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 47413d02f49..e7b9d93cd8e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -42,7 +42,7 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi; 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.configuration.DockerConfiguration.DockerHostConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConnectionConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; @@ -362,7 +362,8 @@ class LifecycleTests { given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(trustBuilder); - createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376"))) + createLifecycle(request, + ResolvedDockerHost.from(new DockerConnectionConfiguration.Host("tcp://192.168.1.2:2376"))) .execute(); if (trustBuilder) { assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-inherit-remote.json")); @@ -384,7 +385,7 @@ class LifecycleTests { given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(trustBuilder); - createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("/var/alt.sock"))) + createLifecycle(request, ResolvedDockerHost.from(new DockerConnectionConfiguration.Host("/var/alt.sock"))) .execute(); if (trustBuilder) { assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-inherit-local.json")); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationTests.java index e5ee7f51b70..d61cac68be0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2025 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. @@ -26,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Wei Jiang * @author Scott Frederick */ +@SuppressWarnings("removal") class DockerConfigurationTests { @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHostTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHostTests.java index a2b8de3f7c4..300a4efa8d0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHostTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/ResolvedDockerHostTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -31,8 +31,6 @@ import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -75,17 +73,6 @@ class ResolvedDockerHostTests { assertThat(dockerHost.getCertificatePath()).isNull(); } - @Test - @DisabledOnOs(OS.WINDOWS) - void resolveWhenDockerHostAddressIsNullReturnsLinuxDefault() throws Exception { - this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - DockerHostConfiguration.forAddress(null)); - assertThat(dockerHost.getAddress()).isEqualTo("/var/run/docker.sock"); - assertThat(dockerHost.isSecure()).isFalse(); - assertThat(dockerHost.getCertificatePath()).isNull(); - } - @Test @DisabledOnOs(OS.WINDOWS) void resolveWhenUsingDefaultContextReturnsLinuxDefault() { @@ -100,7 +87,7 @@ class ResolvedDockerHostTests { void resolveWhenDockerHostAddressIsLocalReturnsAddress(@TempDir Path tempDir) throws IOException { String socketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath().toString(); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - DockerHostConfiguration.forAddress(socketFilePath)); + new DockerConnectionConfiguration.Host(socketFilePath)); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -112,7 +99,7 @@ class ResolvedDockerHostTests { void resolveWhenDockerHostAddressIsLocalWithSchemeReturnsAddress(@TempDir Path tempDir) throws IOException { String socketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath().toString(); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - DockerHostConfiguration.forAddress("unix://" + socketFilePath)); + new DockerConnectionConfiguration.Host("unix://" + socketFilePath)); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -123,7 +110,7 @@ class ResolvedDockerHostTests { @Test void resolveWhenDockerHostAddressIsHttpReturnsAddress() { ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - DockerHostConfiguration.forAddress("http://docker.example.com")); + new DockerConnectionConfiguration.Host("http://docker.example.com")); assertThat(dockerHost.isLocalFileReference()).isFalse(); assertThat(dockerHost.isRemote()).isTrue(); assertThat(dockerHost.getAddress()).isEqualTo("http://docker.example.com"); @@ -134,7 +121,7 @@ class ResolvedDockerHostTests { @Test void resolveWhenDockerHostAddressIsHttpsReturnsAddress() { ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - DockerHostConfiguration.forAddress("https://docker.example.com", true, "/cert-path")); + new DockerConnectionConfiguration.Host("https://docker.example.com", true, "/cert-path")); assertThat(dockerHost.isLocalFileReference()).isFalse(); assertThat(dockerHost.isRemote()).isTrue(); assertThat(dockerHost.getAddress()).isEqualTo("https://docker.example.com"); @@ -145,7 +132,7 @@ class ResolvedDockerHostTests { @Test void resolveWhenDockerHostAddressIsTcpReturnsAddress() { ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - DockerHostConfiguration.forAddress("tcp://192.168.99.100:2376", true, "/cert-path")); + new DockerConnectionConfiguration.Host("tcp://192.168.99.100:2376", true, "/cert-path")); assertThat(dockerHost.isLocalFileReference()).isFalse(); assertThat(dockerHost.isRemote()).isTrue(); assertThat(dockerHost.getAddress()).isEqualTo("tcp://192.168.99.100:2376"); @@ -158,7 +145,7 @@ class ResolvedDockerHostTests { String socketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath().toString(); this.environment.put("DOCKER_HOST", socketFilePath); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - DockerHostConfiguration.forAddress("/unused")); + new DockerConnectionConfiguration.Host("/unused")); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -171,7 +158,7 @@ class ResolvedDockerHostTests { String socketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath().toString(); this.environment.put("DOCKER_HOST", "unix://" + socketFilePath); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - DockerHostConfiguration.forAddress("/unused")); + new DockerConnectionConfiguration.Host("/unused")); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -185,7 +172,7 @@ class ResolvedDockerHostTests { this.environment.put("DOCKER_TLS_VERIFY", "1"); this.environment.put("DOCKER_CERT_PATH", "/cert-path"); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - DockerHostConfiguration.forAddress("tcp://1.1.1.1")); + new DockerConnectionConfiguration.Host("tcp://1.1.1.1")); assertThat(dockerHost.isLocalFileReference()).isFalse(); assertThat(dockerHost.isRemote()).isTrue(); assertThat(dockerHost.getAddress()).isEqualTo("tcp://192.168.99.100:2376"); @@ -197,7 +184,7 @@ class ResolvedDockerHostTests { void resolveWithDockerHostContextReturnsAddress() throws Exception { this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - DockerHostConfiguration.forContext("test-context")); + new DockerConnectionConfiguration.Context("test-context")); assertThat(dockerHost.getAddress()).isEqualTo("/home/user/.docker/docker.sock"); assertThat(dockerHost.isSecure()).isTrue(); assertThat(dockerHost.getCertificatePath()).isNotNull(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java index a383d0240ec..693b0e89165 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -23,7 +23,7 @@ import java.nio.file.Path; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConnectionConfiguration; import static org.assertj.core.api.Assertions.assertThat; @@ -37,21 +37,21 @@ class HttpTransportTests { @Test void createWhenDockerHostVariableIsAddressReturnsRemote() { - HttpTransport transport = HttpTransport.create(DockerHostConfiguration.forAddress("tcp://192.168.1.0")); + HttpTransport transport = HttpTransport.create(new DockerConnectionConfiguration.Host("tcp://192.168.1.0")); assertThat(transport).isInstanceOf(RemoteHttpClientTransport.class); } @Test void createWhenDockerHostVariableIsFileReturnsLocal(@TempDir Path tempDir) throws IOException { String dummySocketFilePath = Files.createTempFile(tempDir, "http-transport", null).toAbsolutePath().toString(); - HttpTransport transport = HttpTransport.create(DockerHostConfiguration.forAddress(dummySocketFilePath)); + HttpTransport transport = HttpTransport.create(new DockerConnectionConfiguration.Host(dummySocketFilePath)); assertThat(transport).isInstanceOf(LocalHttpClientTransport.class); } @Test void createWhenDockerHostVariableIsUnixSchemePrefixedFileReturnsLocal(@TempDir Path tempDir) throws IOException { String dummySocketFilePath = "unix://" + Files.createTempFile(tempDir, "http-transport", null).toAbsolutePath(); - HttpTransport transport = HttpTransport.create(DockerHostConfiguration.forAddress(dummySocketFilePath)); + HttpTransport transport = HttpTransport.create(new DockerConnectionConfiguration.Host(dummySocketFilePath)); assertThat(transport).isInstanceOf(LocalHttpClientTransport.class); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransportTests.java index 81cd780c5b0..bcd4fad6f7f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransportTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransportTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -24,7 +24,7 @@ import java.nio.file.Paths; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConnectionConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; import static org.assertj.core.api.Assertions.assertThat; @@ -39,7 +39,7 @@ class LocalHttpClientTransportTests { @Test void createWhenDockerHostIsFileReturnsTransport(@TempDir Path tempDir) throws IOException { String socketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath().toString(); - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(DockerHostConfiguration.forAddress(socketFilePath)); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerConnectionConfiguration.Host(socketFilePath)); LocalHttpClientTransport transport = LocalHttpClientTransport.create(dockerHost); assertThat(transport).isNotNull(); assertThat(transport.getHost().toHostString()).isEqualTo(socketFilePath); @@ -48,7 +48,7 @@ class LocalHttpClientTransportTests { @Test void createWhenDockerHostIsFileThatDoesNotExistReturnsTransport(@TempDir Path tempDir) { String socketFilePath = Paths.get(tempDir.toString(), "dummy").toAbsolutePath().toString(); - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(DockerHostConfiguration.forAddress(socketFilePath)); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerConnectionConfiguration.Host(socketFilePath)); LocalHttpClientTransport transport = LocalHttpClientTransport.create(dockerHost); assertThat(transport).isNotNull(); assertThat(transport.getHost().toHostString()).isEqualTo(socketFilePath); @@ -57,7 +57,7 @@ class LocalHttpClientTransportTests { @Test void createWhenDockerHostIsAddressReturnsTransport() { ResolvedDockerHost dockerHost = ResolvedDockerHost - .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376")); + .from(new DockerConnectionConfiguration.Host("tcp://192.168.1.2:2376")); LocalHttpClientTransport transport = LocalHttpClientTransport.create(dockerHost); assertThat(transport).isNotNull(); assertThat(transport.getHost().toHostString()).isEqualTo("tcp://192.168.1.2:2376"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java index 87767ed49ac..6de2b285822 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java @@ -23,7 +23,7 @@ import javax.net.ssl.SSLContext; import org.apache.hc.core5.http.HttpHost; import org.junit.jupiter.api.Test; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConnectionConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory; @@ -42,14 +42,7 @@ class RemoteHttpClientTransportTests { @Test void createIfPossibleWhenDockerHostIsNotSetReturnsNull() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(null); - RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); - assertThat(transport).isNull(); - } - - @Test - void createIfPossibleWhenDockerHostIsDefaultReturnsNull() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(DockerHostConfiguration.forAddress(null)); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from((DockerConnectionConfiguration) null); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport).isNull(); } @@ -57,7 +50,7 @@ class RemoteHttpClientTransportTests { @Test void createIfPossibleWhenDockerHostIsFileReturnsNull() { ResolvedDockerHost dockerHost = ResolvedDockerHost - .from(DockerHostConfiguration.forAddress("unix:///var/run/socket.sock")); + .from(new DockerConnectionConfiguration.Host("unix:///var/run/socket.sock")); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport).isNull(); } @@ -65,7 +58,7 @@ class RemoteHttpClientTransportTests { @Test void createIfPossibleWhenDockerHostIsAddressReturnsTransport() { ResolvedDockerHost dockerHost = ResolvedDockerHost - .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376")); + .from(new DockerConnectionConfiguration.Host("tcp://192.168.1.2:2376")); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport).isNotNull(); } @@ -73,7 +66,7 @@ class RemoteHttpClientTransportTests { @Test void createIfPossibleWhenNoTlsVerifyUsesHttp() { ResolvedDockerHost dockerHost = ResolvedDockerHost - .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376")); + .from(new DockerConnectionConfiguration.Host("tcp://192.168.1.2:2376")); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport.getHost()).satisfies(hostOf("http", "192.168.1.2", 2376)); } @@ -83,7 +76,7 @@ class RemoteHttpClientTransportTests { SslContextFactory sslContextFactory = mock(SslContextFactory.class); given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault()); ResolvedDockerHost dockerHost = ResolvedDockerHost - .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376", true, "/test-cert-path")); + .from(new DockerConnectionConfiguration.Host("tcp://192.168.1.2:2376", true, "/test-cert-path")); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost, sslContextFactory); assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376)); } @@ -91,7 +84,7 @@ class RemoteHttpClientTransportTests { @Test void createIfPossibleWhenTlsVerifyWithMissingCertPathThrowsException() { ResolvedDockerHost dockerHost = ResolvedDockerHost - .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376", true, null)); + .from(new DockerConnectionConfiguration.Host("tcp://192.168.1.2:2376", true, null)); assertThatIllegalStateException().isThrownBy(() -> RemoteHttpClientTransport.createIfPossible(dockerHost)) .withMessageContaining("Docker host TLS verification requires trust material"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java index ffed3ddba17..62843a38152 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -26,7 +26,8 @@ import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.build.BuilderDockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication; /** * Encapsulates Docker configuration options. @@ -113,13 +114,13 @@ public abstract class DockerSpec { } /** - * Returns this configuration as a {@link DockerConfiguration} instance. This method - * should only be called when the configuration is complete and will no longer be - * changed. + * Returns this configuration as a {@link BuilderDockerConfiguration} instance. This + * method should only be called when the configuration is complete and will no longer + * be changed. * @return the Docker configuration */ - DockerConfiguration asDockerConfiguration() { - DockerConfiguration dockerConfiguration = new DockerConfiguration(); + BuilderDockerConfiguration asDockerConfiguration() { + BuilderDockerConfiguration dockerConfiguration = new BuilderDockerConfiguration(); dockerConfiguration = customizeHost(dockerConfiguration); dockerConfiguration = dockerConfiguration.withBindHostToBuilder(getBindHostToBuilder().get()); dockerConfiguration = customizeBuilderAuthentication(dockerConfiguration); @@ -127,7 +128,7 @@ public abstract class DockerSpec { return dockerConfiguration; } - private DockerConfiguration customizeHost(DockerConfiguration dockerConfiguration) { + private BuilderDockerConfiguration customizeHost(BuilderDockerConfiguration dockerConfiguration) { String context = getContext().getOrNull(); String host = getHost().getOrNull(); if (context != null && host != null) { @@ -143,36 +144,30 @@ public abstract class DockerSpec { return dockerConfiguration; } - private DockerConfiguration customizeBuilderAuthentication(DockerConfiguration dockerConfiguration) { - if (this.builderRegistry == null || this.builderRegistry.hasEmptyAuth()) { - return dockerConfiguration; - } - if (this.builderRegistry.hasTokenAuth() && !this.builderRegistry.hasUserAuth()) { - return dockerConfiguration.withBuilderRegistryTokenAuthentication(this.builderRegistry.getToken().get()); - } - if (this.builderRegistry.hasUserAuth() && !this.builderRegistry.hasTokenAuth()) { - return dockerConfiguration.withBuilderRegistryUserAuthentication(this.builderRegistry.getUsername().get(), - this.builderRegistry.getPassword().get(), this.builderRegistry.getUrl().getOrNull(), - this.builderRegistry.getEmail().getOrNull()); - } - throw new GradleException( - "Invalid Docker builder registry configuration, either token or username/password must be provided"); + private BuilderDockerConfiguration customizeBuilderAuthentication(BuilderDockerConfiguration dockerConfiguration) { + return dockerConfiguration + .withBuilderRegistryAuthentication(getRegistryAuthentication("builder", this.builderRegistry, null)); + } + + private BuilderDockerConfiguration customizePublishAuthentication(BuilderDockerConfiguration dockerConfiguration) { + return dockerConfiguration.withPublishRegistryAuthentication( + getRegistryAuthentication("publish", this.publishRegistry, DockerRegistryAuthentication.EMPTY_USER)); } - private DockerConfiguration customizePublishAuthentication(DockerConfiguration dockerConfiguration) { - if (this.publishRegistry == null || this.publishRegistry.hasEmptyAuth()) { - return dockerConfiguration.withEmptyPublishRegistryAuthentication(); + private DockerRegistryAuthentication getRegistryAuthentication(String type, DockerRegistrySpec registry, + DockerRegistryAuthentication fallback) { + if (registry == null || registry.hasEmptyAuth()) { + return fallback; } - if (this.publishRegistry.hasTokenAuth() && !this.publishRegistry.hasUserAuth()) { - return dockerConfiguration.withPublishRegistryTokenAuthentication(this.publishRegistry.getToken().get()); + if (registry.hasTokenAuth() && !registry.hasUserAuth()) { + return DockerRegistryAuthentication.token(registry.getToken().get()); } - if (this.publishRegistry.hasUserAuth() && !this.publishRegistry.hasTokenAuth()) { - return dockerConfiguration.withPublishRegistryUserAuthentication(this.publishRegistry.getUsername().get(), - this.publishRegistry.getPassword().get(), this.publishRegistry.getUrl().getOrNull(), - this.publishRegistry.getEmail().getOrNull()); + if (registry.hasUserAuth() && !registry.hasTokenAuth()) { + return DockerRegistryAuthentication.user(registry.getUsername().get(), registry.getPassword().get(), + registry.getUrl().getOrNull(), registry.getEmail().getOrNull()); } - throw new GradleException( - "Invalid Docker publish registry configuration, either token or username/password must be provided"); + throw new GradleException("Invalid Docker " + type + + " registry configuration, either token or username/password must be provided"); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java index 3252cedb2da..f968dee3f6f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -24,8 +24,8 @@ import org.junit.jupiter.api.BeforeEach; 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.DockerConfiguration.DockerHostConfiguration; +import org.springframework.boot.buildpack.platform.build.BuilderDockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConnectionConfiguration; import org.springframework.boot.gradle.junit.GradleProjectBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -52,10 +52,10 @@ class DockerSpecTests { @Test void asDockerConfigurationWithDefaults() { - DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - assertThat(dockerConfiguration.getHost()).isNull(); - assertThat(dockerConfiguration.getBuilderRegistryAuthentication()).isNull(); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); + assertThat(dockerConfiguration.connection()).isNull(); + assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") .contains("\"email\" : \"\"") @@ -67,15 +67,14 @@ class DockerSpecTests { this.dockerSpec.getHost().set("docker.example.com"); this.dockerSpec.getTlsVerify().set(true); this.dockerSpec.getCertPath().set("/tmp/ca-cert"); - DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - DockerHostConfiguration host = dockerConfiguration.getHost(); - assertThat(host.getAddress()).isEqualTo("docker.example.com"); - assertThat(host.isSecure()).isTrue(); - assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert"); - assertThat(host.getContext()).isNull(); - assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse(); - assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); + DockerConnectionConfiguration.Host host = (DockerConnectionConfiguration.Host) dockerConfiguration.connection(); + assertThat(host.address()).isEqualTo("docker.example.com"); + assertThat(host.secure()).isTrue(); + assertThat(host.certificatePath()).isEqualTo("/tmp/ca-cert"); + assertThat(dockerConfiguration.bindHostToBuilder()).isFalse(); + assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") .contains("\"email\" : \"\"") @@ -85,15 +84,14 @@ class DockerSpecTests { @Test void asDockerConfigurationWithHostConfigurationNoTlsVerify() { this.dockerSpec.getHost().set("docker.example.com"); - DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - DockerHostConfiguration host = dockerConfiguration.getHost(); - assertThat(host.getAddress()).isEqualTo("docker.example.com"); - assertThat(host.isSecure()).isFalse(); - assertThat(host.getCertificatePath()).isNull(); - assertThat(host.getContext()).isNull(); - assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse(); - assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); + DockerConnectionConfiguration.Host host = (DockerConnectionConfiguration.Host) dockerConfiguration.connection(); + assertThat(host.address()).isEqualTo("docker.example.com"); + assertThat(host.secure()).isFalse(); + assertThat(host.certificatePath()).isNull(); + assertThat(dockerConfiguration.bindHostToBuilder()).isFalse(); + assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") .contains("\"email\" : \"\"") @@ -103,15 +101,13 @@ class DockerSpecTests { @Test void asDockerConfigurationWithContextConfiguration() { this.dockerSpec.getContext().set("test-context"); - DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - DockerHostConfiguration host = dockerConfiguration.getHost(); - assertThat(host.getContext()).isEqualTo("test-context"); - assertThat(host.getAddress()).isNull(); - assertThat(host.isSecure()).isFalse(); - assertThat(host.getCertificatePath()).isNull(); - assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse(); - assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); + DockerConnectionConfiguration.Context host = (DockerConnectionConfiguration.Context) dockerConfiguration + .connection(); + assertThat(host.context()).isEqualTo("test-context"); + assertThat(dockerConfiguration.bindHostToBuilder()).isFalse(); + assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") .contains("\"email\" : \"\"") @@ -130,14 +126,14 @@ class DockerSpecTests { void asDockerConfigurationWithBindHostToBuilder() { this.dockerSpec.getHost().set("docker.example.com"); this.dockerSpec.getBindHostToBuilder().set(true); - DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - DockerHostConfiguration host = dockerConfiguration.getHost(); - assertThat(host.getAddress()).isEqualTo("docker.example.com"); - assertThat(host.isSecure()).isFalse(); - assertThat(host.getCertificatePath()).isNull(); - assertThat(dockerConfiguration.isBindHostToBuilder()).isTrue(); - assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); + DockerConnectionConfiguration.Host host = (DockerConnectionConfiguration.Host) dockerConfiguration.connection(); + assertThat(host.address()).isEqualTo("docker.example.com"); + assertThat(host.secure()).isFalse(); + assertThat(host.certificatePath()).isNull(); + assertThat(dockerConfiguration.bindHostToBuilder()).isTrue(); + assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") .contains("\"email\" : \"\"") @@ -158,18 +154,18 @@ class DockerSpecTests { registry.getUrl().set("https://docker2.example.com"); registry.getEmail().set("docker2@example.com"); }); - DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); + assertThat(decoded(dockerConfiguration.builderRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"user1\"") .contains("\"password\" : \"secret1\"") .contains("\"email\" : \"docker1@example.com\"") .contains("\"serveraddress\" : \"https://docker1.example.com\""); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"user2\"") .contains("\"password\" : \"secret2\"") .contains("\"email\" : \"docker2@example.com\"") .contains("\"serveraddress\" : \"https://docker2.example.com\""); - assertThat(this.dockerSpec.asDockerConfiguration().getHost()).isNull(); + assertThat(dockerConfiguration.connection()).isNull(); } @Test @@ -198,10 +194,10 @@ class DockerSpecTests { void asDockerConfigurationWithTokenAuth() { this.dockerSpec.builderRegistry((registry) -> registry.getToken().set("token1")); this.dockerSpec.publishRegistry((registry) -> registry.getToken().set("token2")); - DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); + assertThat(decoded(dockerConfiguration.builderRegistryAuthentication().getAuthHeader())) .contains("\"identitytoken\" : \"token1\""); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"identitytoken\" : \"token2\""); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java index 36b45af6ed0..74f8aaec2c7 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java @@ -38,10 +38,10 @@ import org.springframework.boot.buildpack.platform.build.AbstractBuildLog; import org.springframework.boot.buildpack.platform.build.BuildLog; import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.Builder; +import org.springframework.boot.buildpack.platform.build.BuilderDockerConfiguration; import org.springframework.boot.buildpack.platform.build.Creator; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.boot.loader.tools.EntryWriter; @@ -262,7 +262,7 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo { Libraries libraries = getLibraries(Collections.emptySet()); try { BuildRequest request = getBuildRequest(libraries); - DockerConfiguration dockerConfiguration = (this.docker != null) + BuilderDockerConfiguration dockerConfiguration = (this.docker != null) ? this.docker.asDockerConfiguration(request.isPublish()) : new Docker().asDockerConfiguration(request.isPublish()); Builder builder = new Builder(new MojoBuildLog(this::getLog), dockerConfiguration); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java index a28403a179c..1f8f7c0708c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -16,7 +16,8 @@ package org.springframework.boot.maven; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.build.BuilderDockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication; /** * Docker configuration options. @@ -137,14 +138,14 @@ public class Docker { } /** - * Returns this configuration as a {@link DockerConfiguration} instance. This method - * should only be called when the configuration is complete and will no longer be - * changed. + * Returns this configuration as a {@link BuilderDockerConfiguration} instance. This + * method should only be called when the configuration is complete and will no longer + * be changed. * @param publish whether the image should be published * @return the Docker configuration */ - DockerConfiguration asDockerConfiguration(boolean publish) { - DockerConfiguration dockerConfiguration = new DockerConfiguration(); + BuilderDockerConfiguration asDockerConfiguration(boolean publish) { + BuilderDockerConfiguration dockerConfiguration = new BuilderDockerConfiguration(); dockerConfiguration = customizeHost(dockerConfiguration); dockerConfiguration = dockerConfiguration.withBindHostToBuilder(this.bindHostToBuilder); dockerConfiguration = customizeBuilderAuthentication(dockerConfiguration); @@ -152,7 +153,7 @@ public class Docker { return dockerConfiguration; } - private DockerConfiguration customizeHost(DockerConfiguration dockerConfiguration) { + private BuilderDockerConfiguration customizeHost(BuilderDockerConfiguration dockerConfiguration) { if (this.context != null && this.host != null) { throw new IllegalArgumentException( "Invalid Docker configuration, either context or host can be provided but not both"); @@ -166,38 +167,34 @@ public class Docker { return dockerConfiguration; } - private DockerConfiguration customizeBuilderAuthentication(DockerConfiguration dockerConfiguration) { - if (this.builderRegistry == null || this.builderRegistry.isEmpty()) { - return dockerConfiguration; - } - if (this.builderRegistry.hasTokenAuth() && !this.builderRegistry.hasUserAuth()) { - return dockerConfiguration.withBuilderRegistryTokenAuthentication(this.builderRegistry.getToken()); - } - 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 builder registry configuration, either token or username/password must be provided"); + private BuilderDockerConfiguration customizeBuilderAuthentication(BuilderDockerConfiguration dockerConfiguration) { + return dockerConfiguration + .withBuilderRegistryAuthentication(getRegistryAuthentication("builder", this.builderRegistry, null)); } - private DockerConfiguration customizePublishAuthentication(DockerConfiguration dockerConfiguration, + private BuilderDockerConfiguration customizePublishAuthentication(BuilderDockerConfiguration dockerConfiguration, boolean publish) { if (!publish) { return dockerConfiguration; } - if (this.publishRegistry == null || this.publishRegistry.isEmpty()) { - return dockerConfiguration.withEmptyPublishRegistryAuthentication(); + return dockerConfiguration.withPublishRegistryAuthentication( + getRegistryAuthentication("publish", this.publishRegistry, DockerRegistryAuthentication.EMPTY_USER)); + } + + private DockerRegistryAuthentication getRegistryAuthentication(String type, DockerRegistry registry, + DockerRegistryAuthentication fallback) { + if (registry == null || registry.isEmpty()) { + return fallback; } - if (this.publishRegistry.hasTokenAuth() && !this.publishRegistry.hasUserAuth()) { - return dockerConfiguration.withPublishRegistryTokenAuthentication(this.publishRegistry.getToken()); + if (registry.hasTokenAuth() && !registry.hasUserAuth()) { + return DockerRegistryAuthentication.token(registry.getToken()); } - if (this.publishRegistry.hasUserAuth() && !this.publishRegistry.hasTokenAuth()) { - return dockerConfiguration.withPublishRegistryUserAuthentication(this.publishRegistry.getUsername(), - this.publishRegistry.getPassword(), this.publishRegistry.getUrl(), this.publishRegistry.getEmail()); + if (registry.hasUserAuth() && !registry.hasTokenAuth()) { + return DockerRegistryAuthentication.user(registry.getUsername(), registry.getPassword(), registry.getUrl(), + registry.getEmail()); } - throw new IllegalArgumentException( - "Invalid Docker publish registry configuration, either token or username/password must be provided"); + throw new IllegalArgumentException("Invalid Docker " + type + + " registry configuration, either token or username/password must be provided"); } /** diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java index 65a162d8b3c..50342a7e567 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -20,8 +20,8 @@ import java.util.Base64; import org.junit.jupiter.api.Test; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; +import org.springframework.boot.buildpack.platform.build.BuilderDockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConnectionConfiguration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -37,10 +37,10 @@ class DockerTests { @Test void asDockerConfigurationWithDefaults() { Docker docker = new Docker(); - DockerConfiguration dockerConfiguration = createDockerConfiguration(docker); - assertThat(dockerConfiguration.getHost()).isNull(); - assertThat(dockerConfiguration.getBuilderRegistryAuthentication()).isNull(); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = createDockerConfiguration(docker); + assertThat(dockerConfiguration.connection()).isNull(); + assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") .contains("\"email\" : \"\"") @@ -53,15 +53,14 @@ class DockerTests { docker.setHost("docker.example.com"); docker.setTlsVerify(true); docker.setCertPath("/tmp/ca-cert"); - DockerConfiguration dockerConfiguration = createDockerConfiguration(docker); - DockerHostConfiguration host = dockerConfiguration.getHost(); - assertThat(host.getAddress()).isEqualTo("docker.example.com"); - assertThat(host.isSecure()).isTrue(); - assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert"); - assertThat(host.getContext()).isNull(); - assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse(); - assertThat(createDockerConfiguration(docker).getBuilderRegistryAuthentication()).isNull(); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = createDockerConfiguration(docker); + DockerConnectionConfiguration.Host host = (DockerConnectionConfiguration.Host) dockerConfiguration.connection(); + assertThat(host.address()).isEqualTo("docker.example.com"); + assertThat(host.secure()).isTrue(); + assertThat(host.certificatePath()).isEqualTo("/tmp/ca-cert"); + assertThat(dockerConfiguration.bindHostToBuilder()).isFalse(); + assertThat(createDockerConfiguration(docker).builderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") .contains("\"email\" : \"\"") @@ -72,15 +71,13 @@ class DockerTests { void asDockerConfigurationWithContextConfiguration() { Docker docker = new Docker(); docker.setContext("test-context"); - DockerConfiguration dockerConfiguration = createDockerConfiguration(docker); - DockerHostConfiguration host = dockerConfiguration.getHost(); - assertThat(host.getContext()).isEqualTo("test-context"); - assertThat(host.getAddress()).isNull(); - assertThat(host.isSecure()).isFalse(); - assertThat(host.getCertificatePath()).isNull(); - assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse(); - assertThat(createDockerConfiguration(docker).getBuilderRegistryAuthentication()).isNull(); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = createDockerConfiguration(docker); + DockerConnectionConfiguration.Context context = (DockerConnectionConfiguration.Context) dockerConfiguration + .connection(); + assertThat(context.context()).isEqualTo("test-context"); + assertThat(dockerConfiguration.bindHostToBuilder()).isFalse(); + assertThat(createDockerConfiguration(docker).builderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") .contains("\"email\" : \"\"") @@ -103,14 +100,14 @@ class DockerTests { docker.setTlsVerify(true); docker.setCertPath("/tmp/ca-cert"); docker.setBindHostToBuilder(true); - DockerConfiguration dockerConfiguration = createDockerConfiguration(docker); - DockerHostConfiguration host = dockerConfiguration.getHost(); - assertThat(host.getAddress()).isEqualTo("docker.example.com"); - assertThat(host.isSecure()).isTrue(); - assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert"); - assertThat(dockerConfiguration.isBindHostToBuilder()).isTrue(); - assertThat(createDockerConfiguration(docker).getBuilderRegistryAuthentication()).isNull(); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = createDockerConfiguration(docker); + DockerConnectionConfiguration.Host host = (DockerConnectionConfiguration.Host) dockerConfiguration.connection(); + assertThat(host.address()).isEqualTo("docker.example.com"); + assertThat(host.secure()).isTrue(); + assertThat(host.certificatePath()).isEqualTo("/tmp/ca-cert"); + assertThat(dockerConfiguration.bindHostToBuilder()).isTrue(); + assertThat(createDockerConfiguration(docker).builderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"\"") .contains("\"password\" : \"\"") .contains("\"email\" : \"\"") @@ -124,13 +121,13 @@ class DockerTests { 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 = createDockerConfiguration(docker); - assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = createDockerConfiguration(docker); + assertThat(decoded(dockerConfiguration.builderRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"user1\"") .contains("\"password\" : \"secret1\"") .contains("\"email\" : \"docker1@example.com\"") .contains("\"serveraddress\" : \"https://docker1.example.com\""); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"username\" : \"user2\"") .contains("\"password\" : \"secret2\"") .contains("\"email\" : \"docker2@example.com\"") @@ -160,8 +157,8 @@ class DockerTests { Docker docker = new Docker(); docker.setPublishRegistry( new Docker.DockerRegistry("user", null, "https://docker.example.com", "docker@example.com")); - DockerConfiguration dockerConfiguration = docker.asDockerConfiguration(false); - assertThat(dockerConfiguration.getPublishRegistryAuthentication()).isNull(); + BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(false); + assertThat(dockerConfiguration.publishRegistryAuthentication()).isNull(); } @Test @@ -169,10 +166,10 @@ class DockerTests { Docker docker = new Docker(); docker.setBuilderRegistry(new Docker.DockerRegistry("token1")); docker.setPublishRegistry(new Docker.DockerRegistry("token2")); - DockerConfiguration dockerConfiguration = createDockerConfiguration(docker); - assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())) + BuilderDockerConfiguration dockerConfiguration = createDockerConfiguration(docker); + assertThat(decoded(dockerConfiguration.builderRegistryAuthentication().getAuthHeader())) .contains("\"identitytoken\" : \"token1\""); - assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader())) .contains("\"identitytoken\" : \"token2\""); } @@ -196,13 +193,12 @@ class DockerTests { dockerRegistry.setToken("token"); Docker docker = new Docker(); docker.setPublishRegistry(dockerRegistry); - DockerConfiguration dockerConfiguration = docker.asDockerConfiguration(false); - assertThat(dockerConfiguration.getPublishRegistryAuthentication()).isNull(); + BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(false); + assertThat(dockerConfiguration.publishRegistryAuthentication()).isNull(); } - private DockerConfiguration createDockerConfiguration(Docker docker) { + private BuilderDockerConfiguration createDockerConfiguration(Docker docker) { return docker.asDockerConfiguration(true); - } String decoded(String value) {