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
This commit is contained in:
Scott Frederick
2020-09-17 14:25:18 -05:00
parent 1c6e37b2ac
commit 54288678d1
18 changed files with 436 additions and 76 deletions

View File

@@ -69,7 +69,7 @@ public class DockerApi {
* Create a new {@link DockerApi} instance.
*/
public DockerApi() {
this(DockerConfiguration.withDefaults());
this(new DockerConfiguration());
}
/**

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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());
}
/**

View File

@@ -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) {

View File

@@ -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;
}
}
}
}

View File

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

View File

@@ -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());
}
}

View File

@@ -47,7 +47,7 @@ class RemoteHttpClientTransportTests {
private final Map<String, String> 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();
}