From 4393a2244ca9e67f84cac74f7a0b17a208b742f2 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 13 Jul 2023 14:27:14 -0600 Subject: [PATCH] Use Docker CLI context to determine daemon host address for image building Configuration files managed by the Docker CLI are now used to determine the host address of the Docker daemon used when building images using buildpacks when a host address is not configured with environment variables or build tool plugin configuration. Closes gh-36445 --- .../buildpack/platform/docker/DockerApi.java | 4 +- .../configuration/DockerConfiguration.java | 63 +++++- .../DockerConfigurationMetadata.java | 211 ++++++++++++++++++ .../docker/configuration/DockerHost.java | 2 +- .../configuration/ResolvedDockerHost.java | 29 ++- .../docker/transport/HttpTransport.java | 5 +- .../transport/LocalHttpClientTransport.java | 44 ++-- .../platform/build/LifecycleTests.java | 8 +- .../DockerConfigurationMetadataTests.java | 104 +++++++++ .../ResolvedDockerHostTests.java | 70 ++++-- .../docker/transport/HttpTransportTests.java | 10 +- .../LocalHttpClientTransportTests.java | 14 +- .../RemoteHttpClientTransportTests.java | 18 +- .../configuration/with-context/config.json | 3 + .../meta.json | 12 + .../with-default-context/config.json | 3 + .../meta.json | 12 + .../docker/cert.pem | 0 .../docker/key.pem | 0 .../configuration/without-context/config.json | 2 + .../docs/asciidoc/packaging-oci-image.adoc | 12 +- .../gradle/tasks/bundling/DockerSpec.java | 14 +- .../tasks/bundling/DockerSpecTests.java | 36 ++- .../docs/asciidoc/packaging-oci-image.adoc | 12 +- .../springframework/boot/maven/Docker.java | 23 +- .../boot/maven/DockerTests.java | 35 ++- 26 files changed, 660 insertions(+), 86 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/config.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/config.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/cert.pem create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/key.pem create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/without-context/config.json 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 4ee957b5da..7365bf2677 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 @@ -40,7 +40,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.hc.core5.net.URIBuilder; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; 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.ContainerConfig; @@ -97,7 +97,7 @@ public class DockerApi { * @param dockerHost the Docker daemon host information * @since 2.4.0 */ - public DockerApi(DockerHost dockerHost) { + public DockerApi(DockerHostConfiguration dockerHost) { this(HttpTransport.create(dockerHost)); } 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 b82a6b28ac..fa47c349fb 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -27,7 +27,7 @@ import org.springframework.util.Assert; */ public final class DockerConfiguration { - private final DockerHost host; + private final DockerHostConfiguration host; private final DockerRegistryAuthentication builderAuthentication; @@ -39,7 +39,7 @@ public final class DockerConfiguration { this(null, null, null, false); } - private DockerConfiguration(DockerHost host, DockerRegistryAuthentication builderAuthentication, + private DockerConfiguration(DockerHostConfiguration host, DockerRegistryAuthentication builderAuthentication, DockerRegistryAuthentication publishAuthentication, boolean bindHostToBuilder) { this.host = host; this.builderAuthentication = builderAuthentication; @@ -47,7 +47,7 @@ public final class DockerConfiguration { this.bindHostToBuilder = bindHostToBuilder; } - public DockerHost getHost() { + public DockerHostConfiguration getHost() { return this.host; } @@ -65,7 +65,13 @@ public final class DockerConfiguration { public DockerConfiguration withHost(String address, boolean secure, String certificatePath) { Assert.notNull(address, "Address must not be null"); - return new DockerConfiguration(new DockerHost(address, secure, certificatePath), this.builderAuthentication, + return new DockerConfiguration(DockerHostConfiguration.forAddress(address, secure, certificatePath), + this.builderAuthentication, this.publishAuthentication, this.bindHostToBuilder); + } + + public DockerConfiguration withContext(String context) { + Assert.notNull(context, "Context must not be null"); + return new DockerConfiguration(DockerHostConfiguration.forContext(context), this.builderAuthentication, this.publishAuthentication, this.bindHostToBuilder); } @@ -107,4 +113,51 @@ public final class DockerConfiguration { new DockerRegistryUserAuthentication("", "", "", ""), this.bindHostToBuilder); } + public static class DockerHostConfiguration { + + private final String address; + + private final String context; + + private final boolean secure; + + private final String certificatePath; + + public DockerHostConfiguration(String address, String context, boolean secure, String certificatePath) { + this.address = address; + this.context = context; + this.secure = secure; + this.certificatePath = certificatePath; + } + + public String getAddress() { + return this.address; + } + + public String getContext() { + return this.context; + } + + public boolean isSecure() { + return this.secure; + } + + public String getCertificatePath() { + return this.certificatePath; + } + + public static DockerHostConfiguration forAddress(String address) { + return new DockerHostConfiguration(address, null, false, null); + } + + public static DockerHostConfiguration forAddress(String address, boolean secure, String certificatePath) { + return new DockerHostConfiguration(address, null, secure, certificatePath); + } + + static DockerHostConfiguration forContext(String context) { + return new DockerHostConfiguration(null, context, false, null); + } + + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java new file mode 100644 index 0000000000..9ab0fef201 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java @@ -0,0 +1,211 @@ +/* + * Copyright 2012-2023 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 java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; + +import org.springframework.boot.buildpack.platform.json.MappedObject; +import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.boot.buildpack.platform.system.Environment; + +/** + * Docker configuration stored in metadata files managed by the Docker CLI. + * + * @author Scott Frederick + */ +final class DockerConfigurationMetadata { + + private static final String DOCKER_CONFIG = "DOCKER_CONFIG"; + + private static final String DEFAULT_CONTEXT = "default"; + + private static final String CONFIG_DIR = ".docker"; + + private static final String CONTEXTS_DIR = "contexts"; + + private static final String META_DIR = "meta"; + + private static final String TLS_DIR = "tls"; + + private static final String DOCKER_ENDPOINT = "docker"; + + private static final String CONFIG_FILE_NAME = "config.json"; + + private static final String CONTEXT_FILE_NAME = "meta.json"; + + private final String configLocation; + + private final DockerConfig config; + + private final DockerContext context; + + private DockerConfigurationMetadata(String configLocation, DockerConfig config, DockerContext context) { + this.configLocation = configLocation; + this.config = config; + this.context = context; + } + + DockerConfig getConfiguration() { + return this.config; + } + + DockerContext getContext() { + return this.context; + } + + DockerContext forContext(String context) { + return createDockerContext(this.configLocation, context); + } + + static DockerConfigurationMetadata from(Environment environment) { + String configLocation = (environment.get(DOCKER_CONFIG) != null) ? environment.get(DOCKER_CONFIG) + : Path.of(System.getProperty("user.home"), CONFIG_DIR).toString(); + DockerConfig dockerConfig = createDockerConfig(configLocation); + DockerContext dockerContext = createDockerContext(configLocation, dockerConfig.getCurrentContext()); + return new DockerConfigurationMetadata(configLocation, dockerConfig, dockerContext); + } + + private static DockerConfig createDockerConfig(String configLocation) { + Path path = Path.of(configLocation, CONFIG_FILE_NAME); + if (!path.toFile().exists()) { + return DockerConfig.empty(); + } + try { + return DockerConfig.fromJson(readPathContent(path)); + } + catch (JsonProcessingException ex) { + throw new IllegalStateException("Error parsing Docker configuration file '" + path + "'", ex); + } + } + + private static DockerContext createDockerContext(String configLocation, String currentContext) { + if (currentContext == null || DEFAULT_CONTEXT.equals(currentContext)) { + return DockerContext.empty(); + } + Path metaPath = Path.of(configLocation, CONTEXTS_DIR, META_DIR, asHash(currentContext), CONTEXT_FILE_NAME); + Path tlsPath = Path.of(configLocation, CONTEXTS_DIR, TLS_DIR, asHash(currentContext), DOCKER_ENDPOINT); + if (!metaPath.toFile().exists()) { + throw new IllegalArgumentException("Docker context '" + currentContext + "' does not exist"); + } + try { + DockerContext context = DockerContext.fromJson(readPathContent(metaPath)); + if (tlsPath.toFile().isDirectory()) { + return context.withTlsPath(tlsPath.toString()); + } + return context; + } + catch (JsonProcessingException ex) { + throw new IllegalStateException("Error parsing Docker context metadata file '" + metaPath + "'", ex); + } + } + + private static String asHash(String currentContext) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(currentContext.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(hash); + } + catch (NoSuchAlgorithmException ex) { + return null; + } + } + + private static String readPathContent(Path path) { + try { + return Files.readString(path); + } + catch (IOException ex) { + throw new IllegalStateException("Error reading Docker configuration file '" + path + "'", ex); + } + } + + static final class DockerConfig extends MappedObject { + + private final String currentContext; + + private DockerConfig(JsonNode node) { + super(node, MethodHandles.lookup()); + this.currentContext = valueAt("/currentContext", String.class); + } + + String getCurrentContext() { + return this.currentContext; + } + + static DockerConfig fromJson(String json) throws JsonProcessingException { + return new DockerConfig(SharedObjectMapper.get().readTree(json)); + } + + static DockerConfig empty() { + return new DockerConfig(NullNode.instance); + } + + } + + static final class DockerContext extends MappedObject { + + private final String dockerHost; + + private final Boolean skipTlsVerify; + + private final String tlsPath; + + private DockerContext(JsonNode node, String tlsPath) { + super(node, MethodHandles.lookup()); + this.dockerHost = valueAt("/Endpoints/" + DOCKER_ENDPOINT + "/Host", String.class); + this.skipTlsVerify = valueAt("/Endpoints/" + DOCKER_ENDPOINT + "/SkipTLSVerify", Boolean.class); + this.tlsPath = tlsPath; + } + + String getDockerHost() { + return this.dockerHost; + } + + Boolean isTlsVerify() { + return this.skipTlsVerify != null && !this.skipTlsVerify; + } + + String getTlsPath() { + return this.tlsPath; + } + + DockerContext withTlsPath(String tlsPath) { + return new DockerContext(this.getNode(), tlsPath); + } + + static DockerContext fromJson(String json) throws JsonProcessingException { + return new DockerContext(SharedObjectMapper.get().readTree(json), null); + } + + static DockerContext empty() { + return new DockerContext(NullNode.instance, null); + } + + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java index 3db5f5541d..8d6d381feb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2022 the original author or authors. + * Copyright 2012-2023 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. 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 95272b19d0..e19d592df0 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-2022 the original author or authors. + * Copyright 2012-2023 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,6 +21,8 @@ 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; /** @@ -43,6 +45,12 @@ public class ResolvedDockerHost extends DockerHost { private static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH"; + private static final String DOCKER_CONTEXT = "DOCKER_CONTEXT"; + + ResolvedDockerHost(String address) { + super(address); + } + ResolvedDockerHost(String address, boolean secure, String certificatePath) { super(address, secure, certificatePath); } @@ -66,11 +74,20 @@ public class ResolvedDockerHost extends DockerHost { } } - public static ResolvedDockerHost from(DockerHost dockerHost) { + public static ResolvedDockerHost from(DockerHostConfiguration dockerHost) { return from(Environment.SYSTEM, dockerHost); } - static ResolvedDockerHost from(Environment environment, DockerHost dockerHost) { + static ResolvedDockerHost from(Environment environment, DockerHostConfiguration dockerHost) { + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(environment); + if (environment.get(DOCKER_CONTEXT) != null) { + DockerContext context = config.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()); + 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)); @@ -79,7 +96,11 @@ public class ResolvedDockerHost extends DockerHost { return new ResolvedDockerHost(dockerHost.getAddress(), dockerHost.isSecure(), dockerHost.getCertificatePath()); } - return new ResolvedDockerHost(Platform.isWindows() ? WINDOWS_NAMED_PIPE_PATH : DOMAIN_SOCKET_PATH, false, null); + if (config.getContext().getDockerHost() != null) { + DockerContext context = config.getContext(); + return new ResolvedDockerHost(context.getDockerHost(), context.isTlsVerify(), context.getTlsPath()); + } + return new ResolvedDockerHost(Platform.isWindows() ? WINDOWS_NAMED_PIPE_PATH : DOMAIN_SOCKET_PATH); } private static boolean isTrue(String value) { 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 4a8461fa09..c428155142 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-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -22,6 +22,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; 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; @@ -93,7 +94,7 @@ public interface HttpTransport { * @param dockerHost the Docker host information * @return a {@link HttpTransport} instance */ - static HttpTransport create(DockerHost dockerHost) { + static HttpTransport create(DockerHostConfiguration dockerHost) { ResolvedDockerHost host = ResolvedDockerHost.from(dockerHost); HttpTransport remote = RemoteHttpClientTransport.createIfPossible(host); return (remote != null) ? remote : LocalHttpClientTransport.create(host); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java index 13241f726c..a5b1035a19 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java @@ -20,23 +20,22 @@ import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.URISyntaxException; import java.net.UnknownHostException; import com.sun.jna.Platform; import org.apache.hc.client5.http.DnsResolver; -import org.apache.hc.client5.http.SchemePortResolver; +import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.classic.HttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.client5.http.routing.HttpRoutePlanner; import org.apache.hc.client5.http.socket.ConnectionSocketFactory; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.config.Registry; import org.apache.hc.core5.http.config.RegistryBuilder; import org.apache.hc.core5.http.protocol.HttpContext; -import org.apache.hc.core5.util.Args; import org.apache.hc.core5.util.TimeValue; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; @@ -51,26 +50,22 @@ import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket; */ final class LocalHttpClientTransport extends HttpClientTransport { - private static final HttpHost LOCAL_DOCKER_HOST; + private static final String DOCKER_SCHEME = "docker"; - static { - try { - LOCAL_DOCKER_HOST = HttpHost.create("docker://localhost"); - } - catch (URISyntaxException ex) { - throw new RuntimeException("Error creating local Docker host address", ex); - } - } + private static final int DEFAULT_DOCKER_PORT = 2376; - private LocalHttpClientTransport(HttpClient client) { - super(client, LOCAL_DOCKER_HOST); + private static final HttpHost LOCAL_DOCKER_HOST = new HttpHost(DOCKER_SCHEME, "localhost", DEFAULT_DOCKER_PORT); + + private LocalHttpClientTransport(HttpClient client, HttpHost host) { + super(client, host); } static LocalHttpClientTransport create(ResolvedDockerHost dockerHost) { HttpClientBuilder builder = HttpClients.custom(); builder.setConnectionManager(new LocalConnectionManager(dockerHost.getAddress())); - builder.setSchemePortResolver(new LocalSchemePortResolver()); - return new LocalHttpClientTransport(builder.build()); + builder.setRoutePlanner(new LocalRoutePlanner()); + HttpHost host = new HttpHost(DOCKER_SCHEME, dockerHost.getAddress()); + return new LocalHttpClientTransport(builder.build(), host); } /** @@ -84,7 +79,7 @@ final class LocalHttpClientTransport extends HttpClientTransport { private static Registry getRegistry(String host) { RegistryBuilder builder = RegistryBuilder.create(); - builder.register("docker", new LocalConnectionSocketFactory(host)); + builder.register(DOCKER_SCHEME, new LocalConnectionSocketFactory(host)); return builder.build(); } @@ -139,20 +134,13 @@ final class LocalHttpClientTransport extends HttpClientTransport { } /** - * {@link SchemePortResolver} for local Docker. + * {@link HttpRoutePlanner} for local Docker. */ - private static class LocalSchemePortResolver implements SchemePortResolver { - - private static final int DEFAULT_DOCKER_PORT = 2376; + private static class LocalRoutePlanner implements HttpRoutePlanner { @Override - public int resolve(HttpHost host) { - Args.notNull(host, "HTTP host"); - String name = host.getSchemeName(); - if ("docker".equals(name)) { - return DEFAULT_DOCKER_PORT; - } - return -1; + public HttpRoute determineRoute(HttpHost target, HttpContext context) { + return new HttpRoute(LOCAL_DOCKER_HOST); } } 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 78ee54874b..d8b03407a3 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 @@ -40,7 +40,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.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; 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; @@ -246,7 +246,8 @@ class LifecycleTests { given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(); - createLifecycle(request, ResolvedDockerHost.from(new DockerHost("tcp://192.168.1.2:2376"))).execute(); + createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376"))) + .execute(); assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-inherit-remote.json")); assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } @@ -257,7 +258,8 @@ class LifecycleTests { given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId()); given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); BuildRequest request = getTestRequest(); - createLifecycle(request, ResolvedDockerHost.from(new DockerHost("/var/alt.sock"))).execute(); + createLifecycle(request, ResolvedDockerHost.from(DockerHostConfiguration.forAddress("/var/alt.sock"))) + .execute(); assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-inherit-local.json")); assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java new file mode 100644 index 0000000000..c619eac316 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2012-2023 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 java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.DockerContext; +import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link DockerConfigurationMetadata}. + * + * @author Scott Frederick + */ +class DockerConfigurationMetadataTests extends AbstractJsonTests { + + private final Map environment = new LinkedHashMap<>(); + + @Test + void configWithContextIsRead() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-context/config.json")); + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get); + assertThat(config.getConfiguration().getCurrentContext()).isEqualTo("test-context"); + assertThat(config.getContext().getDockerHost()).isEqualTo("unix:///home/user/.docker/docker.sock"); + assertThat(config.getContext().isTlsVerify()).isFalse(); + assertThat(config.getContext().getTlsPath()).isNull(); + } + + @Test + void configWithoutContextIsRead() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("without-context/config.json")); + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get); + assertThat(config.getConfiguration().getCurrentContext()).isNull(); + assertThat(config.getContext().getDockerHost()).isNull(); + assertThat(config.getContext().isTlsVerify()).isFalse(); + assertThat(config.getContext().getTlsPath()).isNull(); + } + + @Test + void configWithDefaultContextIsRead() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get); + assertThat(config.getConfiguration().getCurrentContext()).isEqualTo("default"); + assertThat(config.getContext().getDockerHost()).isNull(); + assertThat(config.getContext().isTlsVerify()).isFalse(); + assertThat(config.getContext().getTlsPath()).isNull(); + } + + @Test + void configIsReadWithProvidedContext() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get); + DockerContext context = config.forContext("test-context"); + assertThat(context.getDockerHost()).isEqualTo("unix:///home/user/.docker/docker.sock"); + assertThat(context.isTlsVerify()).isTrue(); + assertThat(context.getTlsPath()).matches("^.*/with-default-context/contexts/tls/[a-zA-z0-9]*/docker$"); + } + + @Test + void invalidContextThrowsException() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); + assertThatIllegalArgumentException() + .isThrownBy(() -> DockerConfigurationMetadata.from(this.environment::get).forContext("invalid-context")) + .withMessageContaining("Docker context 'invalid-context' does not exist"); + } + + @Test + void configIsEmptyWhenConfigFileDoesNotExist() { + this.environment.put("DOCKER_CONFIG", "docker-config-dummy-path"); + DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get); + assertThat(config.getConfiguration().getCurrentContext()).isNull(); + assertThat(config.getContext().getDockerHost()).isNull(); + assertThat(config.getContext().isTlsVerify()).isFalse(); + } + + private String pathToResource(String resource) throws URISyntaxException { + URL url = getClass().getResource(resource); + return Paths.get(url.toURI()).getParent().toAbsolutePath().toString(); + } + +} 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 30a1c35830..1312998497 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-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -17,8 +17,11 @@ package org.springframework.boot.buildpack.platform.docker.configuration; import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.LinkedHashMap; import java.util.Map; @@ -28,6 +31,8 @@ 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; /** @@ -41,7 +46,8 @@ class ResolvedDockerHostTests { @Test @DisabledOnOs(OS.WINDOWS) - void resolveWhenDockerHostIsNullReturnsLinuxDefault() { + void resolveWhenDockerHostIsNullReturnsLinuxDefault() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, null); assertThat(dockerHost.getAddress()).isEqualTo("/var/run/docker.sock"); assertThat(dockerHost.isSecure()).isFalse(); @@ -50,7 +56,8 @@ class ResolvedDockerHostTests { @Test @EnabledOnOs(OS.WINDOWS) - void resolveWhenDockerHostIsNullReturnsWindowsDefault() { + void resolveWhenDockerHostIsNullReturnsWindowsDefault() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json")); ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, null); assertThat(dockerHost.getAddress()).isEqualTo("//./pipe/docker_engine"); assertThat(dockerHost.isSecure()).isFalse(); @@ -59,8 +66,10 @@ class ResolvedDockerHostTests { @Test @DisabledOnOs(OS.WINDOWS) - void resolveWhenDockerHostAddressIsNullReturnsLinuxDefault() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, new DockerHost(null)); + 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(); @@ -70,7 +79,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, - new DockerHost(socketFilePath, false, null)); + DockerHostConfiguration.forAddress(socketFilePath)); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -82,7 +91,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, - new DockerHost("unix://" + socketFilePath, false, null)); + DockerHostConfiguration.forAddress("unix://" + socketFilePath)); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -93,7 +102,7 @@ class ResolvedDockerHostTests { @Test void resolveWhenDockerHostAddressIsHttpReturnsAddress() { ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - new DockerHost("http://docker.example.com", false, null)); + DockerHostConfiguration.forAddress("http://docker.example.com")); assertThat(dockerHost.isLocalFileReference()).isFalse(); assertThat(dockerHost.isRemote()).isTrue(); assertThat(dockerHost.getAddress()).isEqualTo("http://docker.example.com"); @@ -104,7 +113,7 @@ class ResolvedDockerHostTests { @Test void resolveWhenDockerHostAddressIsHttpsReturnsAddress() { ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - new DockerHost("https://docker.example.com", true, "/cert-path")); + DockerHostConfiguration.forAddress("https://docker.example.com", true, "/cert-path")); assertThat(dockerHost.isLocalFileReference()).isFalse(); assertThat(dockerHost.isRemote()).isTrue(); assertThat(dockerHost.getAddress()).isEqualTo("https://docker.example.com"); @@ -115,7 +124,7 @@ class ResolvedDockerHostTests { @Test void resolveWhenDockerHostAddressIsTcpReturnsAddress() { ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, - new DockerHost("tcp://192.168.99.100:2376", true, "/cert-path")); + DockerHostConfiguration.forAddress("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"); @@ -128,7 +137,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, - new DockerHost("/unused", true, "/unused")); + DockerHostConfiguration.forAddress("/unused")); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -141,7 +150,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, - new DockerHost("/unused", true, "/unused")); + DockerHostConfiguration.forAddress("/unused")); assertThat(dockerHost.isLocalFileReference()).isTrue(); assertThat(dockerHost.isRemote()).isFalse(); assertThat(dockerHost.getAddress()).isEqualTo(socketFilePath); @@ -155,7 +164,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, - new DockerHost("tcp://1.1.1.1", false, "/unused")); + DockerHostConfiguration.forAddress("tcp://1.1.1.1")); assertThat(dockerHost.isLocalFileReference()).isFalse(); assertThat(dockerHost.isRemote()).isTrue(); assertThat(dockerHost.getAddress()).isEqualTo("tcp://192.168.99.100:2376"); @@ -163,4 +172,39 @@ class ResolvedDockerHostTests { assertThat(dockerHost.getCertificatePath()).isEqualTo("/cert-path"); } + @Test + 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")); + assertThat(dockerHost.getAddress()).isEqualTo("/home/user/.docker/docker.sock"); + assertThat(dockerHost.isSecure()).isTrue(); + assertThat(dockerHost.getCertificatePath()).isNotNull(); + } + + @Test + void resolveWithDockerConfigMetadataContextReturnsAddress() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-context/config.json")); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, null); + assertThat(dockerHost.getAddress()).isEqualTo("/home/user/.docker/docker.sock"); + assertThat(dockerHost.isSecure()).isFalse(); + assertThat(dockerHost.getCertificatePath()).isNull(); + } + + @Test + void resolveWhenEnvironmentHasAddressAndContextPrefersContext() throws Exception { + this.environment.put("DOCKER_CONFIG", pathToResource("with-context/config.json")); + this.environment.put("DOCKER_CONTEXT", "test-context"); + this.environment.put("DOCKER_HOST", "notused"); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(this.environment::get, null); + assertThat(dockerHost.getAddress()).isEqualTo("/home/user/.docker/docker.sock"); + assertThat(dockerHost.isSecure()).isFalse(); + assertThat(dockerHost.getCertificatePath()).isNull(); + } + + private String pathToResource(String resource) throws URISyntaxException { + URL url = getClass().getResource(resource); + return Paths.get(url.toURI()).getParent().toAbsolutePath().toString(); + } + } 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 c04cd5e719..a383d0240e 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-2022 the original author or authors. + * Copyright 2012-2023 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.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import static org.assertj.core.api.Assertions.assertThat; @@ -37,21 +37,21 @@ class HttpTransportTests { @Test void createWhenDockerHostVariableIsAddressReturnsRemote() { - HttpTransport transport = HttpTransport.create(new DockerHost("tcp://192.168.1.0")); + HttpTransport transport = HttpTransport.create(DockerHostConfiguration.forAddress("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(new DockerHost(dummySocketFilePath)); + HttpTransport transport = HttpTransport.create(DockerHostConfiguration.forAddress(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(new DockerHost(dummySocketFilePath)); + HttpTransport transport = HttpTransport.create(DockerHostConfiguration.forAddress(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 78ff1d0c71..81cd780c5b 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-2022 the original author or authors. + * Copyright 2012-2023 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.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; import static org.assertj.core.api.Assertions.assertThat; @@ -39,24 +39,28 @@ class LocalHttpClientTransportTests { @Test void createWhenDockerHostIsFileReturnsTransport(@TempDir Path tempDir) throws IOException { String socketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath().toString(); - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost(socketFilePath)); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(DockerHostConfiguration.forAddress(socketFilePath)); LocalHttpClientTransport transport = LocalHttpClientTransport.create(dockerHost); assertThat(transport).isNotNull(); + assertThat(transport.getHost().toHostString()).isEqualTo(socketFilePath); } @Test void createWhenDockerHostIsFileThatDoesNotExistReturnsTransport(@TempDir Path tempDir) { String socketFilePath = Paths.get(tempDir.toString(), "dummy").toAbsolutePath().toString(); - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost(socketFilePath)); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(DockerHostConfiguration.forAddress(socketFilePath)); LocalHttpClientTransport transport = LocalHttpClientTransport.create(dockerHost); assertThat(transport).isNotNull(); + assertThat(transport.getHost().toHostString()).isEqualTo(socketFilePath); } @Test void createWhenDockerHostIsAddressReturnsTransport() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost("tcp://192.168.1.2:2376")); + ResolvedDockerHost dockerHost = ResolvedDockerHost + .from(DockerHostConfiguration.forAddress("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 a56373709e..529709d5cc 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.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost; import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory; @@ -49,28 +49,31 @@ class RemoteHttpClientTransportTests { @Test void createIfPossibleWhenDockerHostIsDefaultReturnsNull() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost(null)); + ResolvedDockerHost dockerHost = ResolvedDockerHost.from(DockerHostConfiguration.forAddress(null)); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport).isNull(); } @Test void createIfPossibleWhenDockerHostIsFileReturnsNull() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost("unix:///var/run/socket.sock")); + ResolvedDockerHost dockerHost = ResolvedDockerHost + .from(DockerHostConfiguration.forAddress("unix:///var/run/socket.sock")); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport).isNull(); } @Test void createIfPossibleWhenDockerHostIsAddressReturnsTransport() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost("tcp://192.168.1.2:2376")); + ResolvedDockerHost dockerHost = ResolvedDockerHost + .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376")); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport).isNotNull(); } @Test void createIfPossibleWhenNoTlsVerifyUsesHttp() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost("tcp://192.168.1.2:2376")); + ResolvedDockerHost dockerHost = ResolvedDockerHost + .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376")); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(dockerHost); assertThat(transport.getHost()).satisfies(hostOf("http", "192.168.1.2", 2376)); } @@ -80,14 +83,15 @@ class RemoteHttpClientTransportTests { SslContextFactory sslContextFactory = mock(SslContextFactory.class); given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault()); ResolvedDockerHost dockerHost = ResolvedDockerHost - .from(new DockerHost("tcp://192.168.1.2:2376", true, "/test-cert-path")); + .from(DockerHostConfiguration.forAddress("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)); } @Test void createIfPossibleWhenTlsVerifyWithMissingCertPathThrowsException() { - ResolvedDockerHost dockerHost = ResolvedDockerHost.from(new DockerHost("tcp://192.168.1.2:2376", true, null)); + ResolvedDockerHost dockerHost = ResolvedDockerHost + .from(DockerHostConfiguration.forAddress("tcp://192.168.1.2:2376", true, null)); assertThatIllegalArgumentException().isThrownBy(() -> RemoteHttpClientTransport.createIfPossible(dockerHost)) .withMessageContaining("Docker host TLS verification requires trust material"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/config.json new file mode 100644 index 0000000000..7e3fa77f5b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/config.json @@ -0,0 +1,3 @@ +{ + "currentContext": "test-context" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json new file mode 100644 index 0000000000..fa4655b1a0 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json @@ -0,0 +1,12 @@ +{ + "Name": "test-context", + "Metadata": { + "Description": "A context for testing" + }, + "Endpoints": { + "docker": { + "Host": "unix:///home/user/.docker/docker.sock", + "SkipTLSVerify": true + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/config.json new file mode 100644 index 0000000000..6eaf50253d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/config.json @@ -0,0 +1,3 @@ +{ + "currentContext": "default" +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json new file mode 100644 index 0000000000..f072aa2647 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/meta/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/meta.json @@ -0,0 +1,12 @@ +{ + "Name": "test-context", + "Metadata": { + "Description": "A context for testing" + }, + "Endpoints": { + "docker": { + "Host": "unix:///home/user/.docker/docker.sock", + "SkipTLSVerify": false + } + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/cert.pem b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/cert.pem new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/key.pem b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-default-context/contexts/tls/ea1b2003cc8155cb8af43960c89a4c1e28777d6fd848ff3422cf375329c2626d/docker/key.pem new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/without-context/config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/without-context/config.json new file mode 100644 index 0000000000..2c63c08510 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/without-context/config.json @@ -0,0 +1,2 @@ +{ +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc index c5e4297293..824c79ee00 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -13,7 +13,8 @@ The task is automatically created when the `java` or `war` plugin is applied and [[build-image.docker-daemon]] == Docker Daemon The `bootBuildImage` task requires access to a Docker daemon. -By default, it will communicate with a Docker daemon over a local connection. +The task will inspect local Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] to determine the current https://docs.docker.com/engine/context/working-with-contexts/[context] and use the context connection information to communicate with a Docker daemon. +If the current context can not be determined or the context does not have connection information, then the task will use a default local connection. This works with https://docs.docker.com/install/[Docker Engine] on all supported platforms without configuration. Environment variables can be set to configure the `bootBuildImage` task to use an alternative local or remote connection. @@ -22,6 +23,12 @@ The following table shows the environment variables and their values: |=== | Environment variable | Description +| DOCKER_CONFIG +| Location of Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] used to determine the current context (defaults to `$HOME/.docker`) + +| DOCKER_CONTEXT +| Name of a https://docs.docker.com/engine/context/working-with-contexts/[context] that should be used to retrieve host information from Docker CLI configuration files (overrides `DOCKER_HOST`) + | DOCKER_HOST | URL containing the host and port for the Docker daemon - for example `tcp://192.168.99.100:2376` @@ -38,6 +45,9 @@ The following table summarizes the available properties: |=== | Property | Description +| `context` +| Name of a https://docs.docker.com/engine/context/working-with-contexts/[context] that should be used to retrieve host information from Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] + | `host` | URL containing the host and port for the Docker daemon - for example `tcp://192.168.99.100:2376` 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 ce3907a4db..ffed3ddba1 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-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -54,6 +54,10 @@ public abstract class DockerSpec { this.publishRegistry = publishRegistry; } + @Input + @Optional + public abstract Property getContext(); + @Input @Optional public abstract Property getHost(); @@ -124,7 +128,15 @@ public abstract class DockerSpec { } private DockerConfiguration customizeHost(DockerConfiguration dockerConfiguration) { + String context = getContext().getOrNull(); String host = getHost().getOrNull(); + if (context != null && host != null) { + throw new GradleException( + "Invalid Docker configuration, either context or host can be provided but not both"); + } + if (context != null) { + return dockerConfiguration.withContext(context); + } if (host != null) { return dockerConfiguration.withHost(host, getTlsVerify().get(), getCertPath().getOrNull()); } 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 7cce2758b0..3252cedb2d 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 @@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import org.springframework.boot.gradle.junit.GradleProjectBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -68,10 +68,11 @@ class DockerSpecTests { this.dockerSpec.getTlsVerify().set(true); this.dockerSpec.getCertPath().set("/tmp/ca-cert"); DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - DockerHost host = dockerConfiguration.getHost(); + 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())) @@ -85,10 +86,29 @@ class DockerSpecTests { void asDockerConfigurationWithHostConfigurationNoTlsVerify() { this.dockerSpec.getHost().set("docker.example.com"); DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - DockerHost host = dockerConfiguration.getHost(); + 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())) + .contains("\"username\" : \"\"") + .contains("\"password\" : \"\"") + .contains("\"email\" : \"\"") + .contains("\"serveraddress\" : \"\""); + } + + @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())) @@ -98,12 +118,20 @@ class DockerSpecTests { .contains("\"serveraddress\" : \"\""); } + @Test + void asDockerConfigurationWithHostAndContextFails() { + this.dockerSpec.getContext().set("test-context"); + this.dockerSpec.getHost().set("docker.example.com"); + assertThatExceptionOfType(GradleException.class).isThrownBy(this.dockerSpec::asDockerConfiguration) + .withMessageContaining("Invalid Docker configuration"); + } + @Test void asDockerConfigurationWithBindHostToBuilder() { this.dockerSpec.getHost().set("docker.example.com"); this.dockerSpec.getBindHostToBuilder().set(true); DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration(); - DockerHost host = dockerConfiguration.getHost(); + DockerHostConfiguration host = dockerConfiguration.getHost(); assertThat(host.getAddress()).isEqualTo("docker.example.com"); assertThat(host.isSecure()).isFalse(); assertThat(host.getCertificatePath()).isNull(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc index f9b084ce7e..09e85abb82 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/asciidoc/packaging-oci-image.adoc @@ -28,7 +28,8 @@ The `spring-boot-devtools` and `spring-boot-docker-compose` modules are automati [[build-image.docker-daemon]] == Docker Daemon The `build-image` goal requires access to a Docker daemon. -By default, it will communicate with a Docker daemon over a local connection. +The goal will inspect local Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] to determine the current https://docs.docker.com/engine/context/working-with-contexts/[context] and use the context connection information to communicate with a Docker daemon. +If the current context can not be determined or the context does not have connection information, then the goal will use a default local connection. This works with https://docs.docker.com/install/[Docker Engine] on all supported platforms without configuration. Environment variables can be set to configure the `build-image` goal to use an alternative local or remote connection. @@ -37,6 +38,12 @@ The following table shows the environment variables and their values: |=== | Environment variable | Description +| DOCKER_CONFIG +| Location of Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] used to determine the current context (defaults to `$HOME/.docker`) + +| DOCKER_CONTEXT +| Name of a https://docs.docker.com/engine/context/working-with-contexts/[context] that should be used to retrieve host information from Docker CLI configuration files (overrides `DOCKER_HOST`) + | DOCKER_HOST | URL containing the host and port for the Docker daemon - for example `tcp://192.168.99.100:2376` @@ -53,6 +60,9 @@ The following table summarizes the available parameters: |=== | Parameter | Description +| `context` +| Name of a https://docs.docker.com/engine/context/working-with-contexts/[context] that should be used to retrieve host information from Docker CLI https://docs.docker.com/engine/reference/commandline/cli/#configuration-files[configuration files] + | `host` | URL containing the host and port for the Docker daemon - for example `tcp://192.168.99.100:2376` 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 78e8b5b89b..53618609d4 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-2022 the original author or authors. + * Copyright 2012-2023 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. @@ -29,6 +29,8 @@ public class Docker { private String host; + private String context; + private boolean tlsVerify; private String certPath; @@ -51,6 +53,18 @@ public class Docker { this.host = host; } + /** + * The Docker context to use to retrieve host configuration. + * @return the Docker context + */ + public String getContext() { + return this.context; + } + + public void setContext(String context) { + this.context = context; + } + /** * Whether the Docker daemon requires TLS communication. * @return {@code true} to enable TLS @@ -138,6 +152,13 @@ public class Docker { } private DockerConfiguration customizeHost(DockerConfiguration dockerConfiguration) { + if (this.context != null && this.host != null) { + throw new IllegalArgumentException( + "Invalid Docker configuration, either context or host can be provided but not both"); + } + if (this.context != null) { + return dockerConfiguration.withContext(this.context); + } if (this.host != null) { return dockerConfiguration.withHost(this.host, this.tlsVerify, this.certPath); } 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 1fd381ee28..f2258b915d 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 @@ -21,7 +21,7 @@ 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.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -54,10 +54,11 @@ class DockerTests { docker.setTlsVerify(true); docker.setCertPath("/tmp/ca-cert"); DockerConfiguration dockerConfiguration = docker.asDockerConfiguration(); - DockerHost host = dockerConfiguration.getHost(); + 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(docker.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) @@ -67,6 +68,34 @@ class DockerTests { .contains("\"serveraddress\" : \"\""); } + @Test + void asDockerConfigurationWithContextConfiguration() { + Docker docker = new Docker(); + docker.setContext("test-context"); + DockerConfiguration dockerConfiguration = docker.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(docker.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull(); + assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader())) + .contains("\"username\" : \"\"") + .contains("\"password\" : \"\"") + .contains("\"email\" : \"\"") + .contains("\"serveraddress\" : \"\""); + } + + @Test + void asDockerConfigurationWithHostAndContextFails() { + Docker docker = new Docker(); + docker.setContext("test-context"); + docker.setHost("docker.example.com"); + assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration) + .withMessageContaining("Invalid Docker configuration"); + } + @Test void asDockerConfigurationWithBindHostToBuilder() { Docker docker = new Docker(); @@ -75,7 +104,7 @@ class DockerTests { docker.setCertPath("/tmp/ca-cert"); docker.setBindHostToBuilder(true); DockerConfiguration dockerConfiguration = docker.asDockerConfiguration(); - DockerHost host = dockerConfiguration.getHost(); + DockerHostConfiguration host = dockerConfiguration.getHost(); assertThat(host.getAddress()).isEqualTo("docker.example.com"); assertThat(host.isSecure()).isTrue(); assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert");