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 7770f58efe..8d633c7f8b 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 @@ -69,7 +69,7 @@ public class DockerApi { * Create a new {@link DockerApi} instance. */ public DockerApi() { - this(DockerConfiguration.withDefaults()); + this(new DockerConfiguration()); } /** 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 134910537e..ca0f354d64 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 @@ -27,30 +27,42 @@ import org.springframework.util.Assert; */ public final class DockerConfiguration { + private final DockerHost host; + private final DockerRegistryAuthentication authentication; - private DockerConfiguration(DockerRegistryAuthentication authentication) { + public DockerConfiguration() { + this(null, null); + } + + private DockerConfiguration(DockerHost host, DockerRegistryAuthentication authentication) { + this.host = host; this.authentication = authentication; } + public DockerHost getHost() { + return this.host; + } + public DockerRegistryAuthentication getRegistryAuthentication() { return this.authentication; } - public static DockerConfiguration withDefaults() { - return new DockerConfiguration(null); + public DockerConfiguration withHost(String address, boolean secure, String certificatePath) { + Assert.notNull(address, "Address must not be null"); + return new DockerConfiguration(new DockerHost(address, secure, certificatePath), this.authentication); } - public static DockerConfiguration withRegistryTokenAuthentication(String token) { + public DockerConfiguration withRegistryTokenAuthentication(String token) { Assert.notNull(token, "Token must not be null"); - return new DockerConfiguration(new DockerRegistryTokenAuthentication(token)); + return new DockerConfiguration(this.host, new DockerRegistryTokenAuthentication(token)); } - public static DockerConfiguration withRegistryUserAuthentication(String username, String password, String url, + public DockerConfiguration withRegistryUserAuthentication(String username, String password, String url, String email) { Assert.notNull(username, "Username must not be null"); Assert.notNull(password, "Password must not be null"); - return new DockerConfiguration(new DockerRegistryUserAuthentication(username, password, url, email)); + return new DockerConfiguration(this.host, new DockerRegistryUserAuthentication(username, password, url, email)); } } 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 new file mode 100644 index 0000000000..facf91c012 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerHost.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.buildpack.platform.docker.configuration; + +/** + * Docker host connection options. + * + * @author Scott Frederick + * @since 2.4.0 + */ +public class DockerHost { + + private final String address; + + private final boolean secure; + + private final String certificatePath; + + protected DockerHost(String address, boolean secure, String certificatePath) { + this.address = address; + this.secure = secure; + this.certificatePath = certificatePath; + } + + public String getAddress() { + return this.address; + } + + public boolean isSecure() { + return this.secure; + } + + public String getCertificatePath() { + return this.certificatePath; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java index ac3d461652..695598a6cc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java @@ -36,7 +36,6 @@ import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.entity.AbstractHttpEntity; import org.apache.http.impl.client.CloseableHttpClient; -import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication; import org.springframework.boot.buildpack.platform.io.Content; import org.springframework.boot.buildpack.platform.io.IOConsumer; @@ -60,12 +59,13 @@ abstract class HttpClientTransport implements HttpTransport { private final String registryAuthHeader; - protected HttpClientTransport(CloseableHttpClient client, HttpHost host, DockerConfiguration dockerConfiguration) { + protected HttpClientTransport(CloseableHttpClient client, HttpHost host, + DockerRegistryAuthentication authentication) { Assert.notNull(client, "Client must not be null"); Assert.notNull(host, "Host must not be null"); this.client = client; this.host = host; - this.registryAuthHeader = buildRegistryAuthHeader(dockerConfiguration); + this.registryAuthHeader = buildRegistryAuthHeader(authentication); } /** @@ -122,9 +122,7 @@ abstract class HttpClientTransport implements HttpTransport { return execute(new HttpDelete(uri)); } - private String buildRegistryAuthHeader(DockerConfiguration dockerConfiguration) { - DockerRegistryAuthentication authentication = (dockerConfiguration != null) - ? dockerConfiguration.getRegistryAuthentication() : null; + private String buildRegistryAuthHeader(DockerRegistryAuthentication authentication) { String authHeader = (authentication != null) ? authentication.createAuthHeader() : null; return (StringUtils.hasText(authHeader)) ? authHeader : null; } 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 d3669526e2..0129de9d37 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 @@ -85,7 +85,7 @@ public interface HttpTransport { * @return a {@link HttpTransport} instance */ static HttpTransport create() { - return create(DockerConfiguration.withDefaults()); + return create(new DockerConfiguration()); } /** @@ -105,7 +105,7 @@ public interface HttpTransport { * @return a {@link HttpTransport} instance */ static HttpTransport create(Environment environment) { - return create(environment, DockerConfiguration.withDefaults()); + return create(environment, new DockerConfiguration()); } /** 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 d6f693c6b0..d83f203f95 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 @@ -39,6 +39,7 @@ import org.apache.http.protocol.HttpContext; import org.apache.http.util.Args; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication; import org.springframework.boot.buildpack.platform.socket.DomainSocket; import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket; import org.springframework.boot.buildpack.platform.system.Environment; @@ -57,15 +58,16 @@ final class LocalHttpClientTransport extends HttpClientTransport { private static final HttpHost LOCAL_DOCKER_HOST = HttpHost.create("docker://localhost"); - private LocalHttpClientTransport(CloseableHttpClient client, DockerConfiguration dockerConfiguration) { - super(client, LOCAL_DOCKER_HOST, dockerConfiguration); + private LocalHttpClientTransport(CloseableHttpClient client, DockerRegistryAuthentication authentication) { + super(client, LOCAL_DOCKER_HOST, authentication); } static LocalHttpClientTransport create(Environment environment, DockerConfiguration dockerConfiguration) { HttpClientBuilder builder = HttpClients.custom(); builder.setConnectionManager(new LocalConnectionManager(socketFilePath(environment))); builder.setSchemePortResolver(new LocalSchemePortResolver()); - return new LocalHttpClientTransport(builder.build(), dockerConfiguration); + return new LocalHttpClientTransport(builder.build(), + (dockerConfiguration != null) ? dockerConfiguration.getRegistryAuthentication() : null); } private static String socketFilePath(Environment environment) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransport.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransport.java index af83e9a3d2..8e81cce0d0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransport.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransport.java @@ -29,6 +29,8 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication; import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory; import org.springframework.boot.buildpack.platform.system.Environment; import org.springframework.util.Assert; @@ -50,8 +52,8 @@ final class RemoteHttpClientTransport extends HttpClientTransport { private static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH"; private RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host, - DockerConfiguration dockerConfiguration) { - super(client, host, dockerConfiguration); + DockerRegistryAuthentication authentication) { + super(client, host, authentication); } static RemoteHttpClientTransport createIfPossible(Environment environment, @@ -61,11 +63,11 @@ final class RemoteHttpClientTransport extends HttpClientTransport { static RemoteHttpClientTransport createIfPossible(Environment environment, DockerConfiguration dockerConfiguration, SslContextFactory sslContextFactory) { - String host = environment.get(DOCKER_HOST); - if (host == null || isLocalFileReference(host)) { + DockerHost host = getHost(environment, dockerConfiguration); + if (host == null || host.getAddress() == null || isLocalFileReference(host.getAddress())) { return null; } - return create(environment, sslContextFactory, HttpHost.create(host), dockerConfiguration); + return create(host, dockerConfiguration, sslContextFactory, HttpHost.create(host.getAddress())); } private static boolean isLocalFileReference(String host) { @@ -78,35 +80,53 @@ final class RemoteHttpClientTransport extends HttpClientTransport { } } - private static RemoteHttpClientTransport create(Environment environment, SslContextFactory sslContextFactory, - HttpHost tcpHost, DockerConfiguration dockerConfiguration) { + private static RemoteHttpClientTransport create(DockerHost host, DockerConfiguration dockerConfiguration, + SslContextFactory sslContextFactory, HttpHost tcpHost) { HttpClientBuilder builder = HttpClients.custom(); - boolean secure = isSecure(environment); - if (secure) { - builder.setSSLSocketFactory(getSecureConnectionSocketFactory(environment, sslContextFactory)); + if (host.isSecure()) { + builder.setSSLSocketFactory(getSecureConnectionSocketFactory(host, sslContextFactory)); } - String scheme = secure ? "https" : "http"; + String scheme = host.isSecure() ? "https" : "http"; HttpHost httpHost = new HttpHost(tcpHost.getHostName(), tcpHost.getPort(), scheme); - return new RemoteHttpClientTransport(builder.build(), httpHost, dockerConfiguration); + return new RemoteHttpClientTransport(builder.build(), httpHost, + (dockerConfiguration != null) ? dockerConfiguration.getRegistryAuthentication() : null); } - private static LayeredConnectionSocketFactory getSecureConnectionSocketFactory(Environment environment, + private static LayeredConnectionSocketFactory getSecureConnectionSocketFactory(DockerHost host, SslContextFactory sslContextFactory) { - String directory = environment.get(DOCKER_CERT_PATH); + String directory = host.getCertificatePath(); Assert.hasText(directory, - () -> DOCKER_TLS_VERIFY + " requires trust material location to be specified with " + DOCKER_CERT_PATH); + () -> "Docker host TLS verification requires trust material location to be specified with certificate path"); SSLContext sslContext = sslContextFactory.forDirectory(directory); return new SSLConnectionSocketFactory(sslContext); } - private static boolean isSecure(Environment environment) { - String secure = environment.get(DOCKER_TLS_VERIFY); - try { - return (secure != null) && (Integer.parseInt(secure) == 1); + private static DockerHost getHost(Environment environment, DockerConfiguration dockerConfiguration) { + if (environment.get(DOCKER_HOST) != null) { + return new EnvironmentDockerHost(environment); } - catch (NumberFormatException ex) { - return false; + if (dockerConfiguration != null && dockerConfiguration.getHost() != null) { + return dockerConfiguration.getHost(); } + return null; + } + + private static class EnvironmentDockerHost extends DockerHost { + + EnvironmentDockerHost(Environment environment) { + super(environment.get(DOCKER_HOST), isTrue(environment.get(DOCKER_TLS_VERIFY)), + environment.get(DOCKER_CERT_PATH)); + } + + private static boolean isTrue(String value) { + try { + return (value != null) && (Integer.parseInt(value) == 1); + } + catch (NumberFormatException ex) { + return false; + } + } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationTests.java index 2786750810..0c17ccdba5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationTests.java @@ -30,13 +30,13 @@ public class DockerConfigurationTests { @Test void createDockerConfigurationWithDefaults() { - DockerConfiguration configuration = DockerConfiguration.withDefaults(); + DockerConfiguration configuration = new DockerConfiguration(); assertThat(configuration.getRegistryAuthentication()).isNull(); } @Test void createDockerConfigurationWithUserAuth() { - DockerConfiguration configuration = DockerConfiguration.withRegistryUserAuthentication("user", "secret", + DockerConfiguration configuration = new DockerConfiguration().withRegistryUserAuthentication("user", "secret", "https://docker.example.com", "docker@example.com"); DockerRegistryAuthentication auth = configuration.getRegistryAuthentication(); assertThat(auth).isNotNull(); @@ -50,7 +50,7 @@ public class DockerConfigurationTests { @Test void createDockerConfigurationWithTokenAuth() { - DockerConfiguration configuration = DockerConfiguration.withRegistryTokenAuthentication("token"); + DockerConfiguration configuration = new DockerConfiguration().withRegistryTokenAuthentication("token"); DockerRegistryAuthentication auth = configuration.getRegistryAuthentication(); assertThat(auth).isNotNull(); assertThat(auth).isInstanceOf(DockerRegistryTokenAuthentication.class); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java index 0400b4ae46..2478277889 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java @@ -239,8 +239,8 @@ class HttpClientTransportTests { @Test void getWithDockerRegistryUserAuthWillSendAuthHeader() throws IOException { - DockerConfiguration dockerConfiguration = DockerConfiguration.withRegistryUserAuthentication("user", "secret", - "https://docker.example.com", "docker@example.com"); + DockerConfiguration dockerConfiguration = new DockerConfiguration().withRegistryUserAuthentication("user", + "secret", "https://docker.example.com", "docker@example.com"); this.http = new TestHttpClientTransport(this.client, dockerConfiguration); givenClientWillReturnResponse(); given(this.entity.getContent()).willReturn(this.content); @@ -261,7 +261,7 @@ class HttpClientTransportTests { @Test void getWithDockerRegistryTokenAuthWillSendAuthHeader() throws IOException { - DockerConfiguration dockerConfiguration = DockerConfiguration.withRegistryTokenAuthentication("token"); + DockerConfiguration dockerConfiguration = new DockerConfiguration().withRegistryTokenAuthentication("token"); this.http = new TestHttpClientTransport(this.client, dockerConfiguration); givenClientWillReturnResponse(); given(this.entity.getContent()).willReturn(this.content); @@ -300,7 +300,7 @@ class HttpClientTransportTests { } protected TestHttpClientTransport(CloseableHttpClient client, DockerConfiguration dockerConfiguration) { - super(client, HttpHost.create("docker://localhost"), dockerConfiguration); + super(client, HttpHost.create("docker://localhost"), dockerConfiguration.getRegistryAuthentication()); } } 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 5939957768..34f2226748 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 @@ -47,7 +47,7 @@ class RemoteHttpClientTransportTests { private final Map environment = new LinkedHashMap<>(); - private final DockerConfiguration dockerConfiguration = DockerConfiguration.withDefaults(); + private final DockerConfiguration dockerConfiguration = new DockerConfiguration(); @Test void createIfPossibleWhenDockerHostIsNotSetReturnsNull() { @@ -57,7 +57,13 @@ class RemoteHttpClientTransportTests { } @Test - void createIfPossibleWhenDockerHostIsFileReturnsNull(@TempDir Path tempDir) throws IOException { + void createIfPossibleWithoutDockerConfigurationReturnsNull() { + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, null); + assertThat(transport).isNull(); + } + + @Test + void createIfPossibleWhenDockerHostInEnvironmentIsFileReturnsNull(@TempDir Path tempDir) throws IOException { String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath() .toString(); this.environment.put("DOCKER_HOST", dummySocketFilePath); @@ -67,7 +73,16 @@ class RemoteHttpClientTransportTests { } @Test - void createIfPossibleWhenDockerHostIsAddressReturnsTransport() { + void createIfPossibleWhenDockerHostInConfigurationIsFileReturnsNull(@TempDir Path tempDir) throws IOException { + String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath() + .toString(); + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, + this.dockerConfiguration.withHost(dummySocketFilePath, false, null)); + assertThat(transport).isNull(); + } + + @Test + void createIfPossibleWhenDockerHostInEnvironmentIsAddressReturnsTransport() { this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, this.dockerConfiguration); @@ -75,12 +90,27 @@ class RemoteHttpClientTransportTests { } @Test - void createIfPossibleWhenTlsVerifyWithMissingCertPathThrowsException() { + void createIfPossibleWhenDockerHostInConfigurationIsAddressReturnsTransport() { + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, + this.dockerConfiguration.withHost("tcp://192.168.1.2:2376", false, null)); + assertThat(transport).isNotNull(); + } + + @Test + void createIfPossibleWhenTlsVerifyInEnvironmentWithMissingCertPathThrowsException() { this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); this.environment.put("DOCKER_TLS_VERIFY", "1"); assertThatIllegalArgumentException().isThrownBy( () -> RemoteHttpClientTransport.createIfPossible(this.environment::get, this.dockerConfiguration)) - .withMessageContaining("DOCKER_CERT_PATH"); + .withMessageContaining("Docker host TLS verification requires trust material"); + } + + @Test + void createIfPossibleWhenTlsVerifyInConfigurationWithMissingCertPathThrowsException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> RemoteHttpClientTransport.createIfPossible(this.environment::get, + this.dockerConfiguration.withHost("tcp://192.168.1.2:2376", true, null))) + .withMessageContaining("Docker host TLS verification requires trust material"); } @Test @@ -92,7 +122,7 @@ class RemoteHttpClientTransportTests { } @Test - void createIfPossibleWhenTlsVerifyUsesHttps() throws Exception { + void createIfPossibleWhenTlsVerifyInEnvironmentUsesHttps() throws Exception { this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); this.environment.put("DOCKER_TLS_VERIFY", "1"); this.environment.put("DOCKER_CERT_PATH", "/test-cert-path"); @@ -103,11 +133,21 @@ class RemoteHttpClientTransportTests { assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376)); } + @Test + void createIfPossibleWhenTlsVerifyInConfigurationUsesHttps() throws Exception { + SslContextFactory sslContextFactory = mock(SslContextFactory.class); + given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault()); + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, + this.dockerConfiguration.withHost("tcp://192.168.1.2:2376", true, "/test-cert-path"), + sslContextFactory); + assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376)); + } + @Test void createIfPossibleWithDockerConfigurationUserAuthReturnsTransport() { this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, - DockerConfiguration.withRegistryUserAuthentication("user", "secret", "http://docker.example.com", + new DockerConfiguration().withRegistryUserAuthentication("user", "secret", "http://docker.example.com", "docker@example.com")); assertThat(transport).isNotNull(); } 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 79bc1ab080..70dd2f2760 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 @@ -34,6 +34,24 @@ The following table shows the environment variables and their values: On Linux and macOS, these environment variables can be set using the command `eval $(minikube docker-env)` after minikube has been started. +Docker daemon connection information can also be provided using `docker` properties in the plugin configuration. +The following table summarizes the available properties: + +|=== +| Property | Description + +| `host` +| URL containing the host and port for the Docker daemon - e.g. `tcp://192.168.99.100:2376` + +| `tlsVerify` +| Enable secure HTTPS protocol when set to `true` (optional) + +| `certPath` +| Path to certificate and key files for HTTPS (required if `tlsVerify` is `true`, ignored otherwise) +|=== + +For more details, see also <>. + [[build-image-docker-registry]] @@ -220,7 +238,21 @@ The image name can be specified on the command line as well, as shown in this ex [[build-image-example-docker]] ==== Docker Configuration -If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided as shown in the following example: +If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` properties as shown in the following example: + +[source,groovy,indent=0,subs="verbatim,attributes",role="primary"] +.Groovy +---- +include::../gradle/packaging/boot-build-image-docker-host.gradle[tags=docker-host] +---- + +[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"] +.Kotlin +---- +include::../gradle/packaging/boot-build-image-docker-host.gradle.kts[tags=docker-host] +---- + +If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided using `docker.registry` properties as shown in the following example: [source,groovy,indent=0,subs="verbatim,attributes",role="primary"] .Groovy @@ -234,7 +266,7 @@ include::../gradle/packaging/boot-build-image-docker-auth-user.gradle[tags=docke include::../gradle/packaging/boot-build-image-docker-auth-user.gradle.kts[tags=docker-auth-user] ---- -If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided as shown in the following example: +If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided using `docker.registry` as shown in the following example: [source,groovy,indent=0,subs="verbatim,attributes",role="primary"] .Groovy diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle new file mode 100644 index 0000000000..5e000607df --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{gradle-project-version}' +} + +bootJar { + mainClassName 'com.example.ExampleApplication' +} + +// tag::docker-host[] +bootBuildImage { + docker { + host = "tcp://192.168.99.100:2376" + tlsVerify = true + certPath = "/home/users/.minikube/certs" + } +} +// end::docker-host[] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle.kts b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle.kts new file mode 100644 index 0000000000..37463b66ee --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/gradle/packaging/boot-build-image-docker-host.gradle.kts @@ -0,0 +1,21 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{gradle-project-version}" +} + +tasks.getByName("bootJar") { + mainClassName = "com.example.ExampleApplication" +} + +// tag::docker-host[] +tasks.getByName("bootBuildImage") { + docker { + host = "tcp://192.168.99.100:2376" + tlsVerify = true + certPath = "/home/users/.minikube/certs" + } +} +// end::docker-host[] 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 7db6300bfd..18aba086d5 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 @@ -35,6 +35,12 @@ import org.springframework.boot.buildpack.platform.docker.configuration.DockerCo */ public class DockerSpec { + private String host; + + private boolean tlsVerify; + + private String certPath; + private final DockerRegistrySpec registry; public DockerSpec() { @@ -45,6 +51,36 @@ public class DockerSpec { this.registry = registry; } + @Input + @Optional + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + @Input + @Optional + public Boolean isTlsVerify() { + return this.tlsVerify; + } + + public void setTlsVerify(boolean tlsVerify) { + this.tlsVerify = tlsVerify; + } + + @Input + @Optional + public String getCertPath() { + return this.certPath; + } + + public void setCertPath(String certPath) { + this.certPath = certPath; + } + /** * Returns the {@link DockerRegistrySpec} that configures registry authentication. * @return the registry spec @@ -77,14 +113,28 @@ public class DockerSpec { * @return the Docker configuration */ DockerConfiguration asDockerConfiguration() { + DockerConfiguration dockerConfiguration = new DockerConfiguration(); + dockerConfiguration = customizeHost(dockerConfiguration); + dockerConfiguration = customizeAuthentication(dockerConfiguration); + return dockerConfiguration; + } + + private DockerConfiguration customizeHost(DockerConfiguration dockerConfiguration) { + if (this.host != null) { + return dockerConfiguration.withHost(this.host, this.tlsVerify, this.certPath); + } + return dockerConfiguration; + } + + private DockerConfiguration customizeAuthentication(DockerConfiguration dockerConfiguration) { if (this.registry == null || this.registry.hasEmptyAuth()) { - return null; + return dockerConfiguration; } if (this.registry.hasTokenAuth() && !this.registry.hasUserAuth()) { - return DockerConfiguration.withRegistryTokenAuthentication(this.registry.getToken()); + return dockerConfiguration.withRegistryTokenAuthentication(this.registry.getToken()); } if (this.registry.hasUserAuth() && !this.registry.hasTokenAuth()) { - return DockerConfiguration.withRegistryUserAuthentication(this.registry.getUsername(), + return dockerConfiguration.withRegistryUserAuthentication(this.registry.getUsername(), this.registry.getPassword(), this.registry.getUrl(), this.registry.getEmail()); } throw new GradleException( 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 565c2e9397..df41e740f6 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 @@ -20,6 +20,7 @@ import org.gradle.api.GradleException; import org.junit.jupiter.api.Test; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication; import org.springframework.util.Base64Utils; @@ -35,15 +36,36 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; public class DockerSpecTests { @Test - void asDockerConfigurationWithoutRegistry() { + void asDockerConfigurationWithDefaults() { DockerSpec dockerSpec = new DockerSpec(); - assertThat(dockerSpec.asDockerConfiguration()).isNull(); + assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull(); + assertThat(dockerSpec.asDockerConfiguration().getRegistryAuthentication()).isNull(); } @Test - void asDockerConfigurationWithEmptyRegistry() { - DockerSpec dockerSpec = new DockerSpec(new DockerSpec.DockerRegistrySpec()); - assertThat(dockerSpec.asDockerConfiguration()).isNull(); + void asDockerConfigurationWithHostConfiguration() { + DockerSpec dockerSpec = new DockerSpec(); + dockerSpec.setHost("docker.example.com"); + dockerSpec.setTlsVerify(true); + dockerSpec.setCertPath("/tmp/ca-cert"); + DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration(); + DockerHost host = dockerConfiguration.getHost(); + assertThat(host.getAddress()).isEqualTo("docker.example.com"); + assertThat(host.isSecure()).isEqualTo(true); + assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert"); + assertThat(dockerSpec.asDockerConfiguration().getRegistryAuthentication()).isNull(); + } + + @Test + void asDockerConfigurationWithHostConfigurationNoTlsVerify() { + DockerSpec dockerSpec = new DockerSpec(); + dockerSpec.setHost("docker.example.com"); + DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration(); + DockerHost host = dockerConfiguration.getHost(); + assertThat(host.getAddress()).isEqualTo("docker.example.com"); + assertThat(host.isSecure()).isEqualTo(false); + assertThat(host.getCertificatePath()).isNull(); + assertThat(dockerSpec.asDockerConfiguration().getRegistryAuthentication()).isNull(); } @Test @@ -61,6 +83,7 @@ public class DockerSpecTests { .contains("\"username\" : \"user\"").contains("\"password\" : \"secret\"") .contains("\"email\" : \"docker@example.com\"") .contains("\"serveraddress\" : \"https://docker.example.com\""); + assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull(); } @Test 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 e5a461df21..4497c1e1db 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 @@ -57,6 +57,24 @@ The following table shows the environment variables and their values: On Linux and macOS, these environment variables can be set using the command `eval $(minikube docker-env)` after minikube has been started. +Docker daemon connection information can also be provided using `docker` parameters in the plugin configuration. +The following table summarizes the available parameters: + +|=== +| Parameter | Description + +| `host` +| URL containing the host and port for the Docker daemon - e.g. `tcp://192.168.99.100:2376` + +| `tlsVerify` +| Enable secure HTTPS protocol when set to `true` (optional) + +| `certPath` +| Path to certificate and key files for HTTPS (required if `tlsVerify` is `true`, ignored otherwise) +|=== + +For more details, see also <>. + [[build-image-docker-registry]] @@ -287,7 +305,31 @@ The image name can be specified on the command line as well, as shown in this ex [[build-image-example-docker]] ==== Docker Configuration -If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided as shown in the following example: +If you need the plugin to communicate with the Docker daemon using a remote connection instead of the default local connection, the connection details can be provided using `docker` parameters as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- + + + + + org.springframework.boot + spring-boot-maven-plugin + {gradle-project-version} + + + tcp://192.168.99.100:2376 + true + /home/user/.minikube/certs + + + + + + +---- + +If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided using `docker.registry` parameters as shown in the following example: [source,xml,indent=0,subs="verbatim,attributes"] ---- @@ -314,7 +356,7 @@ If the builder or run image are stored in a private Docker registry that support ---- -If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided as shown in the following example: +If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided using `docker.registry` parameters as shown in the following example: [source,xml,indent=0,subs="verbatim,attributes"] ---- 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 eb413c54a6..46c249d25a 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 @@ -27,8 +27,38 @@ import org.springframework.boot.buildpack.platform.docker.configuration.DockerCo */ public class Docker { + private String host; + + private boolean tlsVerify; + + private String certPath; + private DockerRegistry registry; + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public boolean isTlsVerify() { + return this.tlsVerify; + } + + public void setTlsVerify(boolean tlsVerify) { + this.tlsVerify = tlsVerify; + } + + public String getCertPath() { + return this.certPath; + } + + public void setCertPath(String certPath) { + this.certPath = certPath; + } + /** * Sets the {@link DockerRegistry} that configures registry authentication. * @param registry the registry configuration @@ -44,17 +74,30 @@ public class Docker { * @return the Docker configuration */ DockerConfiguration asDockerConfiguration() { + DockerConfiguration dockerConfiguration = new DockerConfiguration(); + dockerConfiguration = customizeHost(dockerConfiguration); + dockerConfiguration = customizeAuthentication(dockerConfiguration); + return dockerConfiguration; + } + + private DockerConfiguration customizeHost(DockerConfiguration dockerConfiguration) { + if (this.host != null) { + return dockerConfiguration.withHost(this.host, this.tlsVerify, this.certPath); + } + return dockerConfiguration; + } + + private DockerConfiguration customizeAuthentication(DockerConfiguration dockerConfiguration) { if (this.registry == null || this.registry.isEmpty()) { - return null; + return dockerConfiguration; } if (this.registry.hasTokenAuth() && !this.registry.hasUserAuth()) { - return DockerConfiguration.withRegistryTokenAuthentication(this.registry.getToken()); + return dockerConfiguration.withRegistryTokenAuthentication(this.registry.getToken()); } if (this.registry.hasUserAuth() && !this.registry.hasTokenAuth()) { - return DockerConfiguration.withRegistryUserAuthentication(this.registry.getUsername(), + return dockerConfiguration.withRegistryUserAuthentication(this.registry.getUsername(), this.registry.getPassword(), this.registry.getUrl(), this.registry.getEmail()); } - throw new IllegalArgumentException( "Invalid Docker registry configuration, either token or username/password must be provided"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java index eec5e792e7..c7f1d39e58 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 @@ -19,6 +19,7 @@ package org.springframework.boot.maven; import org.junit.jupiter.api.Test; import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; +import org.springframework.boot.buildpack.platform.docker.configuration.DockerHost; import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication; import org.springframework.util.Base64Utils; @@ -34,17 +35,24 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException public class DockerTests { @Test - void asDockerConfigurationWithoutRegistry() { + void asDockerConfigurationWithDefaults() { Docker docker = new Docker(); - assertThat(docker.asDockerConfiguration()).isNull(); + assertThat(docker.asDockerConfiguration().getHost()).isNull(); + assertThat(docker.asDockerConfiguration().getRegistryAuthentication()).isNull(); } @Test - void asDockerConfigurationWithEmptyRegistry() { - Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry(); + void asDockerConfigurationWithHostConfiguration() { Docker docker = new Docker(); - docker.setRegistry(dockerRegistry); - assertThat(docker.asDockerConfiguration()).isNull(); + docker.setHost("docker.example.com"); + docker.setTlsVerify(true); + docker.setCertPath("/tmp/ca-cert"); + DockerConfiguration dockerConfiguration = docker.asDockerConfiguration(); + DockerHost host = dockerConfiguration.getHost(); + assertThat(host.getAddress()).isEqualTo("docker.example.com"); + assertThat(host.isSecure()).isEqualTo(true); + assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert"); + assertThat(docker.asDockerConfiguration().getRegistryAuthentication()).isNull(); } @Test