Commit 54288678 authored by Scott Frederick's avatar Scott Frederick

Configure Docker host in build plugins

This commit adds the ability to configure the Maven and Gradle
plugins to use a remote Docker daemon using build file
configuration, as an alternative to setting environment variables
to specify remote host connection details.

Fixes gh-23400
parent 1c6e37b2
...@@ -69,7 +69,7 @@ public class DockerApi { ...@@ -69,7 +69,7 @@ public class DockerApi {
* Create a new {@link DockerApi} instance. * Create a new {@link DockerApi} instance.
*/ */
public DockerApi() { public DockerApi() {
this(DockerConfiguration.withDefaults()); this(new DockerConfiguration());
} }
/** /**
......
...@@ -27,30 +27,42 @@ import org.springframework.util.Assert; ...@@ -27,30 +27,42 @@ import org.springframework.util.Assert;
*/ */
public final class DockerConfiguration { public final class DockerConfiguration {
private final DockerHost host;
private final DockerRegistryAuthentication authentication; 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; this.authentication = authentication;
} }
public DockerHost getHost() {
return this.host;
}
public DockerRegistryAuthentication getRegistryAuthentication() { public DockerRegistryAuthentication getRegistryAuthentication() {
return this.authentication; return this.authentication;
} }
public static DockerConfiguration withDefaults() { public DockerConfiguration withHost(String address, boolean secure, String certificatePath) {
return new DockerConfiguration(null); 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"); 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) { String email) {
Assert.notNull(username, "Username must not be null"); Assert.notNull(username, "Username must not be null");
Assert.notNull(password, "Password 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));
} }
} }
/*
* 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;
}
}
...@@ -36,7 +36,6 @@ import org.apache.http.client.methods.HttpUriRequest; ...@@ -36,7 +36,6 @@ import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.AbstractHttpEntity; import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.impl.client.CloseableHttpClient; 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.docker.configuration.DockerRegistryAuthentication;
import org.springframework.boot.buildpack.platform.io.Content; import org.springframework.boot.buildpack.platform.io.Content;
import org.springframework.boot.buildpack.platform.io.IOConsumer; import org.springframework.boot.buildpack.platform.io.IOConsumer;
...@@ -60,12 +59,13 @@ abstract class HttpClientTransport implements HttpTransport { ...@@ -60,12 +59,13 @@ abstract class HttpClientTransport implements HttpTransport {
private final String registryAuthHeader; 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(client, "Client must not be null");
Assert.notNull(host, "Host must not be null"); Assert.notNull(host, "Host must not be null");
this.client = client; this.client = client;
this.host = host; this.host = host;
this.registryAuthHeader = buildRegistryAuthHeader(dockerConfiguration); this.registryAuthHeader = buildRegistryAuthHeader(authentication);
} }
/** /**
...@@ -122,9 +122,7 @@ abstract class HttpClientTransport implements HttpTransport { ...@@ -122,9 +122,7 @@ abstract class HttpClientTransport implements HttpTransport {
return execute(new HttpDelete(uri)); return execute(new HttpDelete(uri));
} }
private String buildRegistryAuthHeader(DockerConfiguration dockerConfiguration) { private String buildRegistryAuthHeader(DockerRegistryAuthentication authentication) {
DockerRegistryAuthentication authentication = (dockerConfiguration != null)
? dockerConfiguration.getRegistryAuthentication() : null;
String authHeader = (authentication != null) ? authentication.createAuthHeader() : null; String authHeader = (authentication != null) ? authentication.createAuthHeader() : null;
return (StringUtils.hasText(authHeader)) ? authHeader : null; return (StringUtils.hasText(authHeader)) ? authHeader : null;
} }
......
...@@ -85,7 +85,7 @@ public interface HttpTransport { ...@@ -85,7 +85,7 @@ public interface HttpTransport {
* @return a {@link HttpTransport} instance * @return a {@link HttpTransport} instance
*/ */
static HttpTransport create() { static HttpTransport create() {
return create(DockerConfiguration.withDefaults()); return create(new DockerConfiguration());
} }
/** /**
...@@ -105,7 +105,7 @@ public interface HttpTransport { ...@@ -105,7 +105,7 @@ public interface HttpTransport {
* @return a {@link HttpTransport} instance * @return a {@link HttpTransport} instance
*/ */
static HttpTransport create(Environment environment) { static HttpTransport create(Environment environment) {
return create(environment, DockerConfiguration.withDefaults()); return create(environment, new DockerConfiguration());
} }
/** /**
......
...@@ -39,6 +39,7 @@ import org.apache.http.protocol.HttpContext; ...@@ -39,6 +39,7 @@ import org.apache.http.protocol.HttpContext;
import org.apache.http.util.Args; import org.apache.http.util.Args;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; 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.DomainSocket;
import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket; import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket;
import org.springframework.boot.buildpack.platform.system.Environment; import org.springframework.boot.buildpack.platform.system.Environment;
...@@ -57,15 +58,16 @@ final class LocalHttpClientTransport extends HttpClientTransport { ...@@ -57,15 +58,16 @@ final class LocalHttpClientTransport extends HttpClientTransport {
private static final HttpHost LOCAL_DOCKER_HOST = HttpHost.create("docker://localhost"); private static final HttpHost LOCAL_DOCKER_HOST = HttpHost.create("docker://localhost");
private LocalHttpClientTransport(CloseableHttpClient client, DockerConfiguration dockerConfiguration) { private LocalHttpClientTransport(CloseableHttpClient client, DockerRegistryAuthentication authentication) {
super(client, LOCAL_DOCKER_HOST, dockerConfiguration); super(client, LOCAL_DOCKER_HOST, authentication);
} }
static LocalHttpClientTransport create(Environment environment, DockerConfiguration dockerConfiguration) { static LocalHttpClientTransport create(Environment environment, DockerConfiguration dockerConfiguration) {
HttpClientBuilder builder = HttpClients.custom(); HttpClientBuilder builder = HttpClients.custom();
builder.setConnectionManager(new LocalConnectionManager(socketFilePath(environment))); builder.setConnectionManager(new LocalConnectionManager(socketFilePath(environment)));
builder.setSchemePortResolver(new LocalSchemePortResolver()); 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) { private static String socketFilePath(Environment environment) {
......
...@@ -29,6 +29,8 @@ import org.apache.http.impl.client.HttpClientBuilder; ...@@ -29,6 +29,8 @@ import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClients;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; 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.docker.ssl.SslContextFactory;
import org.springframework.boot.buildpack.platform.system.Environment; import org.springframework.boot.buildpack.platform.system.Environment;
import org.springframework.util.Assert; import org.springframework.util.Assert;
...@@ -50,8 +52,8 @@ final class RemoteHttpClientTransport extends HttpClientTransport { ...@@ -50,8 +52,8 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
private static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH"; private static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH";
private RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host, private RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host,
DockerConfiguration dockerConfiguration) { DockerRegistryAuthentication authentication) {
super(client, host, dockerConfiguration); super(client, host, authentication);
} }
static RemoteHttpClientTransport createIfPossible(Environment environment, static RemoteHttpClientTransport createIfPossible(Environment environment,
...@@ -61,11 +63,11 @@ final class RemoteHttpClientTransport extends HttpClientTransport { ...@@ -61,11 +63,11 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
static RemoteHttpClientTransport createIfPossible(Environment environment, DockerConfiguration dockerConfiguration, static RemoteHttpClientTransport createIfPossible(Environment environment, DockerConfiguration dockerConfiguration,
SslContextFactory sslContextFactory) { SslContextFactory sslContextFactory) {
String host = environment.get(DOCKER_HOST); DockerHost host = getHost(environment, dockerConfiguration);
if (host == null || isLocalFileReference(host)) { if (host == null || host.getAddress() == null || isLocalFileReference(host.getAddress())) {
return null; 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) { private static boolean isLocalFileReference(String host) {
...@@ -78,35 +80,53 @@ final class RemoteHttpClientTransport extends HttpClientTransport { ...@@ -78,35 +80,53 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
} }
} }
private static RemoteHttpClientTransport create(Environment environment, SslContextFactory sslContextFactory, private static RemoteHttpClientTransport create(DockerHost host, DockerConfiguration dockerConfiguration,
HttpHost tcpHost, DockerConfiguration dockerConfiguration) { SslContextFactory sslContextFactory, HttpHost tcpHost) {
HttpClientBuilder builder = HttpClients.custom(); HttpClientBuilder builder = HttpClients.custom();
boolean secure = isSecure(environment); if (host.isSecure()) {
if (secure) { builder.setSSLSocketFactory(getSecureConnectionSocketFactory(host, sslContextFactory));
builder.setSSLSocketFactory(getSecureConnectionSocketFactory(environment, sslContextFactory));
} }
String scheme = secure ? "https" : "http"; String scheme = host.isSecure() ? "https" : "http";
HttpHost httpHost = new HttpHost(tcpHost.getHostName(), tcpHost.getPort(), scheme); 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) { SslContextFactory sslContextFactory) {
String directory = environment.get(DOCKER_CERT_PATH); String directory = host.getCertificatePath();
Assert.hasText(directory, 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); SSLContext sslContext = sslContextFactory.forDirectory(directory);
return new SSLConnectionSocketFactory(sslContext); return new SSLConnectionSocketFactory(sslContext);
} }
private static boolean isSecure(Environment environment) { private static DockerHost getHost(Environment environment, DockerConfiguration dockerConfiguration) {
String secure = environment.get(DOCKER_TLS_VERIFY); if (environment.get(DOCKER_HOST) != null) {
return new EnvironmentDockerHost(environment);
}
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 { try {
return (secure != null) && (Integer.parseInt(secure) == 1); return (value != null) && (Integer.parseInt(value) == 1);
} }
catch (NumberFormatException ex) { catch (NumberFormatException ex) {
return false; return false;
} }
} }
}
} }
...@@ -30,13 +30,13 @@ public class DockerConfigurationTests { ...@@ -30,13 +30,13 @@ public class DockerConfigurationTests {
@Test @Test
void createDockerConfigurationWithDefaults() { void createDockerConfigurationWithDefaults() {
DockerConfiguration configuration = DockerConfiguration.withDefaults(); DockerConfiguration configuration = new DockerConfiguration();
assertThat(configuration.getRegistryAuthentication()).isNull(); assertThat(configuration.getRegistryAuthentication()).isNull();
} }
@Test @Test
void createDockerConfigurationWithUserAuth() { void createDockerConfigurationWithUserAuth() {
DockerConfiguration configuration = DockerConfiguration.withRegistryUserAuthentication("user", "secret", DockerConfiguration configuration = new DockerConfiguration().withRegistryUserAuthentication("user", "secret",
"https://docker.example.com", "docker@example.com"); "https://docker.example.com", "docker@example.com");
DockerRegistryAuthentication auth = configuration.getRegistryAuthentication(); DockerRegistryAuthentication auth = configuration.getRegistryAuthentication();
assertThat(auth).isNotNull(); assertThat(auth).isNotNull();
...@@ -50,7 +50,7 @@ public class DockerConfigurationTests { ...@@ -50,7 +50,7 @@ public class DockerConfigurationTests {
@Test @Test
void createDockerConfigurationWithTokenAuth() { void createDockerConfigurationWithTokenAuth() {
DockerConfiguration configuration = DockerConfiguration.withRegistryTokenAuthentication("token"); DockerConfiguration configuration = new DockerConfiguration().withRegistryTokenAuthentication("token");
DockerRegistryAuthentication auth = configuration.getRegistryAuthentication(); DockerRegistryAuthentication auth = configuration.getRegistryAuthentication();
assertThat(auth).isNotNull(); assertThat(auth).isNotNull();
assertThat(auth).isInstanceOf(DockerRegistryTokenAuthentication.class); assertThat(auth).isInstanceOf(DockerRegistryTokenAuthentication.class);
......
...@@ -239,8 +239,8 @@ class HttpClientTransportTests { ...@@ -239,8 +239,8 @@ class HttpClientTransportTests {
@Test @Test
void getWithDockerRegistryUserAuthWillSendAuthHeader() throws IOException { void getWithDockerRegistryUserAuthWillSendAuthHeader() throws IOException {
DockerConfiguration dockerConfiguration = DockerConfiguration.withRegistryUserAuthentication("user", "secret", DockerConfiguration dockerConfiguration = new DockerConfiguration().withRegistryUserAuthentication("user",
"https://docker.example.com", "docker@example.com"); "secret", "https://docker.example.com", "docker@example.com");
this.http = new TestHttpClientTransport(this.client, dockerConfiguration); this.http = new TestHttpClientTransport(this.client, dockerConfiguration);
givenClientWillReturnResponse(); givenClientWillReturnResponse();
given(this.entity.getContent()).willReturn(this.content); given(this.entity.getContent()).willReturn(this.content);
...@@ -261,7 +261,7 @@ class HttpClientTransportTests { ...@@ -261,7 +261,7 @@ class HttpClientTransportTests {
@Test @Test
void getWithDockerRegistryTokenAuthWillSendAuthHeader() throws IOException { void getWithDockerRegistryTokenAuthWillSendAuthHeader() throws IOException {
DockerConfiguration dockerConfiguration = DockerConfiguration.withRegistryTokenAuthentication("token"); DockerConfiguration dockerConfiguration = new DockerConfiguration().withRegistryTokenAuthentication("token");
this.http = new TestHttpClientTransport(this.client, dockerConfiguration); this.http = new TestHttpClientTransport(this.client, dockerConfiguration);
givenClientWillReturnResponse(); givenClientWillReturnResponse();
given(this.entity.getContent()).willReturn(this.content); given(this.entity.getContent()).willReturn(this.content);
...@@ -300,7 +300,7 @@ class HttpClientTransportTests { ...@@ -300,7 +300,7 @@ class HttpClientTransportTests {
} }
protected TestHttpClientTransport(CloseableHttpClient client, DockerConfiguration dockerConfiguration) { protected TestHttpClientTransport(CloseableHttpClient client, DockerConfiguration dockerConfiguration) {
super(client, HttpHost.create("docker://localhost"), dockerConfiguration); super(client, HttpHost.create("docker://localhost"), dockerConfiguration.getRegistryAuthentication());
} }
} }
......
...@@ -47,7 +47,7 @@ class RemoteHttpClientTransportTests { ...@@ -47,7 +47,7 @@ class RemoteHttpClientTransportTests {
private final Map<String, String> environment = new LinkedHashMap<>(); private final Map<String, String> environment = new LinkedHashMap<>();
private final DockerConfiguration dockerConfiguration = DockerConfiguration.withDefaults(); private final DockerConfiguration dockerConfiguration = new DockerConfiguration();
@Test @Test
void createIfPossibleWhenDockerHostIsNotSetReturnsNull() { void createIfPossibleWhenDockerHostIsNotSetReturnsNull() {
...@@ -57,7 +57,13 @@ class RemoteHttpClientTransportTests { ...@@ -57,7 +57,13 @@ class RemoteHttpClientTransportTests {
} }
@Test @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() String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath()
.toString(); .toString();
this.environment.put("DOCKER_HOST", dummySocketFilePath); this.environment.put("DOCKER_HOST", dummySocketFilePath);
...@@ -67,7 +73,16 @@ class RemoteHttpClientTransportTests { ...@@ -67,7 +73,16 @@ class RemoteHttpClientTransportTests {
} }
@Test @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"); this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
this.dockerConfiguration); this.dockerConfiguration);
...@@ -75,12 +90,27 @@ class RemoteHttpClientTransportTests { ...@@ -75,12 +90,27 @@ class RemoteHttpClientTransportTests {
} }
@Test @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_HOST", "tcp://192.168.1.2:2376");
this.environment.put("DOCKER_TLS_VERIFY", "1"); this.environment.put("DOCKER_TLS_VERIFY", "1");
assertThatIllegalArgumentException().isThrownBy( assertThatIllegalArgumentException().isThrownBy(
() -> RemoteHttpClientTransport.createIfPossible(this.environment::get, this.dockerConfiguration)) () -> 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 @Test
...@@ -92,7 +122,7 @@ class RemoteHttpClientTransportTests { ...@@ -92,7 +122,7 @@ class RemoteHttpClientTransportTests {
} }
@Test @Test
void createIfPossibleWhenTlsVerifyUsesHttps() throws Exception { void createIfPossibleWhenTlsVerifyInEnvironmentUsesHttps() throws Exception {
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
this.environment.put("DOCKER_TLS_VERIFY", "1"); this.environment.put("DOCKER_TLS_VERIFY", "1");
this.environment.put("DOCKER_CERT_PATH", "/test-cert-path"); this.environment.put("DOCKER_CERT_PATH", "/test-cert-path");
...@@ -103,11 +133,21 @@ class RemoteHttpClientTransportTests { ...@@ -103,11 +133,21 @@ class RemoteHttpClientTransportTests {
assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376)); 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 @Test
void createIfPossibleWithDockerConfigurationUserAuthReturnsTransport() { void createIfPossibleWithDockerConfigurationUserAuthReturnsTransport() {
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, 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")); "docker@example.com"));
assertThat(transport).isNotNull(); assertThat(transport).isNotNull();
} }
......
...@@ -34,6 +34,24 @@ The following table shows the environment variables and their values: ...@@ -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. 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-example-docker,examples>>.
[[build-image-docker-registry]] [[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 ...@@ -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]] [[build-image-example-docker]]
==== Docker Configuration ==== 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"] [source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy .Groovy
...@@ -234,7 +266,7 @@ include::../gradle/packaging/boot-build-image-docker-auth-user.gradle[tags=docke ...@@ -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] 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"] [source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy .Groovy
......
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[]
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
plugins {
java
id("org.springframework.boot") version "{gradle-project-version}"
}
tasks.getByName<BootJar>("bootJar") {
mainClassName = "com.example.ExampleApplication"
}
// tag::docker-host[]
tasks.getByName<BootBuildImage>("bootBuildImage") {
docker {
host = "tcp://192.168.99.100:2376"
tlsVerify = true
certPath = "/home/users/.minikube/certs"
}
}
// end::docker-host[]
...@@ -35,6 +35,12 @@ import org.springframework.boot.buildpack.platform.docker.configuration.DockerCo ...@@ -35,6 +35,12 @@ import org.springframework.boot.buildpack.platform.docker.configuration.DockerCo
*/ */
public class DockerSpec { public class DockerSpec {
private String host;
private boolean tlsVerify;
private String certPath;
private final DockerRegistrySpec registry; private final DockerRegistrySpec registry;
public DockerSpec() { public DockerSpec() {
...@@ -45,6 +51,36 @@ public class DockerSpec { ...@@ -45,6 +51,36 @@ public class DockerSpec {
this.registry = registry; 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. * Returns the {@link DockerRegistrySpec} that configures registry authentication.
* @return the registry spec * @return the registry spec
...@@ -77,14 +113,28 @@ public class DockerSpec { ...@@ -77,14 +113,28 @@ public class DockerSpec {
* @return the Docker configuration * @return the Docker configuration
*/ */
DockerConfiguration asDockerConfiguration() { 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()) { if (this.registry == null || this.registry.hasEmptyAuth()) {
return null; return dockerConfiguration;
} }
if (this.registry.hasTokenAuth() && !this.registry.hasUserAuth()) { 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()) { 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()); this.registry.getPassword(), this.registry.getUrl(), this.registry.getEmail());
} }
throw new GradleException( throw new GradleException(
......
...@@ -20,6 +20,7 @@ import org.gradle.api.GradleException; ...@@ -20,6 +20,7 @@ import org.gradle.api.GradleException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; 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.configuration.DockerRegistryAuthentication;
import org.springframework.util.Base64Utils; import org.springframework.util.Base64Utils;
...@@ -35,15 +36,36 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; ...@@ -35,15 +36,36 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
public class DockerSpecTests { public class DockerSpecTests {
@Test @Test
void asDockerConfigurationWithoutRegistry() { void asDockerConfigurationWithDefaults() {
DockerSpec dockerSpec = new DockerSpec(); DockerSpec dockerSpec = new DockerSpec();
assertThat(dockerSpec.asDockerConfiguration()).isNull(); assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull();
assertThat(dockerSpec.asDockerConfiguration().getRegistryAuthentication()).isNull();
} }
@Test @Test
void asDockerConfigurationWithEmptyRegistry() { void asDockerConfigurationWithHostConfiguration() {
DockerSpec dockerSpec = new DockerSpec(new DockerSpec.DockerRegistrySpec()); DockerSpec dockerSpec = new DockerSpec();
assertThat(dockerSpec.asDockerConfiguration()).isNull(); 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 @Test
...@@ -61,6 +83,7 @@ public class DockerSpecTests { ...@@ -61,6 +83,7 @@ public class DockerSpecTests {
.contains("\"username\" : \"user\"").contains("\"password\" : \"secret\"") .contains("\"username\" : \"user\"").contains("\"password\" : \"secret\"")
.contains("\"email\" : \"docker@example.com\"") .contains("\"email\" : \"docker@example.com\"")
.contains("\"serveraddress\" : \"https://docker.example.com\""); .contains("\"serveraddress\" : \"https://docker.example.com\"");
assertThat(dockerSpec.asDockerConfiguration().getHost()).isNull();
} }
@Test @Test
......
...@@ -57,6 +57,24 @@ The following table shows the environment variables and their values: ...@@ -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. 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-example-docker,examples>>.
[[build-image-docker-registry]] [[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 ...@@ -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]] [[build-image-example-docker]]
==== Docker Configuration ==== 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"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<docker>
<host>tcp://192.168.99.100:2376</host>
<tlsVerify>true</tlsVerify>
<certPath>/home/user/.minikube/certs</certPath>
</docker>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided using `docker.registry` parameters as shown in the following example:
[source,xml,indent=0,subs="verbatim,attributes"] [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 ...@@ -314,7 +356,7 @@ If the builder or run image are stored in a private Docker registry that support
</project> </project>
---- ----
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"] [source,xml,indent=0,subs="verbatim,attributes"]
---- ----
......
...@@ -27,8 +27,38 @@ import org.springframework.boot.buildpack.platform.docker.configuration.DockerCo ...@@ -27,8 +27,38 @@ import org.springframework.boot.buildpack.platform.docker.configuration.DockerCo
*/ */
public class Docker { public class Docker {
private String host;
private boolean tlsVerify;
private String certPath;
private DockerRegistry registry; 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. * Sets the {@link DockerRegistry} that configures registry authentication.
* @param registry the registry configuration * @param registry the registry configuration
...@@ -44,17 +74,30 @@ public class Docker { ...@@ -44,17 +74,30 @@ public class Docker {
* @return the Docker configuration * @return the Docker configuration
*/ */
DockerConfiguration asDockerConfiguration() { 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()) { if (this.registry == null || this.registry.isEmpty()) {
return null; return dockerConfiguration;
} }
if (this.registry.hasTokenAuth() && !this.registry.hasUserAuth()) { 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()) { 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()); this.registry.getPassword(), this.registry.getUrl(), this.registry.getEmail());
} }
throw new IllegalArgumentException( throw new IllegalArgumentException(
"Invalid Docker registry configuration, either token or username/password must be provided"); "Invalid Docker registry configuration, either token or username/password must be provided");
} }
......
...@@ -19,6 +19,7 @@ package org.springframework.boot.maven; ...@@ -19,6 +19,7 @@ package org.springframework.boot.maven;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration; 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.configuration.DockerRegistryAuthentication;
import org.springframework.util.Base64Utils; import org.springframework.util.Base64Utils;
...@@ -34,17 +35,24 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException ...@@ -34,17 +35,24 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
public class DockerTests { public class DockerTests {
@Test @Test
void asDockerConfigurationWithoutRegistry() { void asDockerConfigurationWithDefaults() {
Docker docker = new Docker(); Docker docker = new Docker();
assertThat(docker.asDockerConfiguration()).isNull(); assertThat(docker.asDockerConfiguration().getHost()).isNull();
assertThat(docker.asDockerConfiguration().getRegistryAuthentication()).isNull();
} }
@Test @Test
void asDockerConfigurationWithEmptyRegistry() { void asDockerConfigurationWithHostConfiguration() {
Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry();
Docker docker = new Docker(); Docker docker = new Docker();
docker.setRegistry(dockerRegistry); docker.setHost("docker.example.com");
assertThat(docker.asDockerConfiguration()).isNull(); 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 @Test
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment