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");