Skip to content

Commit ed6e542

Browse files
Support remote Docker daemon for building images
Prior to this commit, the build plugin goal/task for building images required a locally running Docker daemon that was accessed via a non-networked socket or pipe. This commit adds support for remote Docker daemons at a location specified by the environment variable `DOCKER_HOST`. Additional environment variables `DOCKER_TLS_VERIFY` and `DOCKER_CERT_PATH` are recognized for configuring a secure TLS connection to the daemon. Fixes gh-20538
1 parent fd05bc2 commit ed6e542

24 files changed

+1400
-55
lines changed

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerApi.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
* Provides access to the limited set of Docker APIs needed by pack.
4545
*
4646
* @author Phillip Webb
47+
* @author Scott Frederick
4748
* @since 2.3.0
4849
*/
4950
public class DockerApi {
@@ -96,7 +97,7 @@ private URI buildUrl(String path, Collection<String> params) {
9697

9798
private URI buildUrl(String path, String... params) {
9899
try {
99-
URIBuilder builder = new URIBuilder("docker://localhost/" + API_VERSION + path);
100+
URIBuilder builder = new URIBuilder("/" + API_VERSION + path);
100101
int param = 0;
101102
while (param < params.length) {
102103
builder.addParameter(params[param++], params[param++]);

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerException.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* Exception throw when the Docker API fails.
2525
*
2626
* @author Phillip Webb
27+
* @author Scott Frederick
2728
* @since 2.3.0
2829
*/
2930
public class DockerException extends RuntimeException {
@@ -34,8 +35,8 @@ public class DockerException extends RuntimeException {
3435

3536
private final Errors errors;
3637

37-
DockerException(URI uri, int statusCode, String reasonPhrase, Errors errors) {
38-
super(buildMessage(uri, statusCode, reasonPhrase, errors));
38+
DockerException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors) {
39+
super(buildMessage(host, uri, statusCode, reasonPhrase, errors));
3940
this.statusCode = statusCode;
4041
this.reasonPhrase = reasonPhrase;
4142
this.errors = errors;
@@ -66,10 +67,11 @@ public Errors getErrors() {
6667
return this.errors;
6768
}
6869

69-
private static String buildMessage(URI uri, int statusCode, String reasonPhrase, Errors errors) {
70+
private static String buildMessage(String host, URI uri, int statusCode, String reasonPhrase, Errors errors) {
71+
Assert.notNull(host, "host must not be null");
7072
Assert.notNull(uri, "URI must not be null");
7173
StringBuilder message = new StringBuilder(
72-
"Docker API call to '" + uri + "' failed with status code " + statusCode);
74+
"Docker API call to '" + host + uri + "' failed with status code " + statusCode);
7375
if (reasonPhrase != null && !reasonPhrase.isEmpty()) {
7476
message.append(" \"" + reasonPhrase + "\"");
7577
}

spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/HttpClientHttp.java

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import org.apache.http.HttpEntity;
2525
import org.apache.http.HttpHeaders;
26+
import org.apache.http.HttpHost;
2627
import org.apache.http.StatusLine;
2728
import org.apache.http.client.HttpClient;
2829
import org.apache.http.client.methods.CloseableHttpResponse;
@@ -34,9 +35,9 @@
3435
import org.apache.http.client.methods.HttpUriRequest;
3536
import org.apache.http.entity.AbstractHttpEntity;
3637
import org.apache.http.impl.client.CloseableHttpClient;
37-
import org.apache.http.impl.client.HttpClientBuilder;
38-
import org.apache.http.impl.client.HttpClients;
3938

39+
import org.springframework.boot.buildpack.platform.docker.httpclient.DelegatingDockerHttpClientConnection;
40+
import org.springframework.boot.buildpack.platform.docker.httpclient.DockerHttpClientConnection;
4041
import org.springframework.boot.buildpack.platform.io.Content;
4142
import org.springframework.boot.buildpack.platform.io.IOConsumer;
4243
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
@@ -50,17 +51,14 @@
5051
*/
5152
class HttpClientHttp implements Http {
5253

53-
private final CloseableHttpClient client;
54+
private final DockerHttpClientConnection clientConnection;
5455

5556
HttpClientHttp() {
56-
HttpClientBuilder builder = HttpClients.custom();
57-
builder.setConnectionManager(new DockerHttpClientConnectionManager());
58-
builder.setSchemePortResolver(new DockerSchemePortResolver());
59-
this.client = builder.build();
57+
this.clientConnection = DelegatingDockerHttpClientConnection.create();
6058
}
6159

62-
HttpClientHttp(CloseableHttpClient client) {
63-
this.client = client;
60+
HttpClientHttp(DockerHttpClientConnection clientConnection) {
61+
this.clientConnection = clientConnection;
6462
}
6563

6664
/**
@@ -90,7 +88,6 @@ public Response post(URI uri) {
9088
* @param writer a content writer
9189
* @return the operation response
9290
*/
93-
9491
@Override
9592
public Response post(URI uri, String contentType, IOConsumer<OutputStream> writer) {
9693
return execute(new HttpPost(uri), contentType, writer);
@@ -103,7 +100,6 @@ public Response post(URI uri, String contentType, IOConsumer<OutputStream> write
103100
* @param writer a content writer
104101
* @return the operation response
105102
*/
106-
107103
@Override
108104
public Response put(URI uri, String contentType, IOConsumer<OutputStream> writer) {
109105
return execute(new HttpPut(uri), contentType, writer);
@@ -114,7 +110,6 @@ public Response put(URI uri, String contentType, IOConsumer<OutputStream> writer
114110
* @param uri the destination URI
115111
* @return the operation response
116112
*/
117-
118113
@Override
119114
public Response delete(URI uri) {
120115
return execute(new HttpDelete(uri));
@@ -128,23 +123,36 @@ private Response execute(HttpEntityEnclosingRequestBase request, String contentT
128123
}
129124

130125
private Response execute(HttpUriRequest request) {
126+
HttpHost host = this.clientConnection.getHttpHost();
127+
CloseableHttpClient client = this.clientConnection.getHttpClient();
128+
131129
try {
132-
CloseableHttpResponse response = this.client.execute(request);
130+
CloseableHttpResponse response = client.execute(host, request);
133131
StatusLine statusLine = response.getStatusLine();
134132
int statusCode = statusLine.getStatusCode();
135133
HttpEntity entity = response.getEntity();
136134

137135
if (statusCode >= 400 && statusCode < 500) {
138-
Errors errors = SharedObjectMapper.get().readValue(entity.getContent(), Errors.class);
139-
throw new DockerException(request.getURI(), statusCode, statusLine.getReasonPhrase(), errors);
136+
throw new DockerException(host.toHostString(), request.getURI(), statusCode,
137+
statusLine.getReasonPhrase(), getErrorsFromResponse(entity));
140138
}
141139
if (statusCode == 500) {
142-
throw new DockerException(request.getURI(), statusCode, statusLine.getReasonPhrase(), null);
140+
throw new DockerException(host.toHostString(), request.getURI(), statusCode,
141+
statusLine.getReasonPhrase(), null);
143142
}
144143
return new HttpClientResponse(response);
145144
}
146145
catch (IOException ioe) {
147-
throw new DockerException(request.getURI(), 500, ioe.getMessage(), null);
146+
throw new DockerException(host.toHostString(), request.getURI(), 500, ioe.getMessage(), null);
147+
}
148+
}
149+
150+
private Errors getErrorsFromResponse(HttpEntity entity) {
151+
try {
152+
return SharedObjectMapper.get().readValue(entity.getContent(), Errors.class);
153+
}
154+
catch (IOException ioe) {
155+
return null;
148156
}
149157
}
150158

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.buildpack.platform.docker.httpclient;
18+
19+
import org.apache.http.HttpHost;
20+
import org.apache.http.client.HttpClient;
21+
import org.apache.http.impl.client.CloseableHttpClient;
22+
23+
/**
24+
* A {@code DockerHttpClientConnection} that determines an appropriate connection to a
25+
* Docker host by detecting whether a remote Docker host is configured or if a default
26+
* local connection should be used.
27+
*
28+
* @author Scott Frederick
29+
* @since 2.3.0
30+
*/
31+
public final class DelegatingDockerHttpClientConnection implements DockerHttpClientConnection {
32+
33+
private static final RemoteEnvironmentDockerHttpClientConnection REMOTE_FACTORY = new RemoteEnvironmentDockerHttpClientConnection();
34+
35+
private static final LocalDockerHttpClientConnection LOCAL_FACTORY = new LocalDockerHttpClientConnection();
36+
37+
private final DockerHttpClientConnection delegate;
38+
39+
private DelegatingDockerHttpClientConnection(DockerHttpClientConnection delegate) {
40+
this.delegate = delegate;
41+
}
42+
43+
/**
44+
* Get an {@link HttpHost} describing the Docker host connection.
45+
* @return the {@code HttpHost}
46+
*/
47+
public HttpHost getHttpHost() {
48+
return this.delegate.getHttpHost();
49+
}
50+
51+
/**
52+
* Get an {@link HttpClient} that can be used to communicate with the Docker host.
53+
* @return the {@code HttpClient}
54+
*/
55+
public CloseableHttpClient getHttpClient() {
56+
return this.delegate.getHttpClient();
57+
}
58+
59+
/**
60+
* Create a {@link DockerHttpClientConnection} by detecting the connection
61+
* configuration.
62+
* @return the {@code DockerHttpClientConnection}
63+
*/
64+
public static DockerHttpClientConnection create() {
65+
if (REMOTE_FACTORY.accept()) {
66+
return new DelegatingDockerHttpClientConnection(REMOTE_FACTORY);
67+
}
68+
if (LOCAL_FACTORY.accept()) {
69+
return new DelegatingDockerHttpClientConnection(LOCAL_FACTORY);
70+
}
71+
return null;
72+
}
73+
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.buildpack.platform.docker.httpclient;
18+
19+
import org.apache.http.HttpHost;
20+
import org.apache.http.client.HttpClient;
21+
import org.apache.http.impl.client.CloseableHttpClient;
22+
23+
/**
24+
* Describes a connection to a Docker host.
25+
*
26+
* @author Scott Frederick
27+
* @since 2.3.0
28+
*/
29+
public interface DockerHttpClientConnection {
30+
31+
/**
32+
* Create an {@link HttpHost} describing the Docker host connection.
33+
* @return the {@code HttpHost}
34+
*/
35+
HttpHost getHttpHost();
36+
37+
/**
38+
* Create an {@link HttpClient} that can be used to communicate with the Docker host.
39+
* @return the {@code HttpClient}
40+
*/
41+
CloseableHttpClient getHttpClient();
42+
43+
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.boot.buildpack.platform.docker;
17+
package org.springframework.boot.buildpack.platform.docker.httpclient;
1818

1919
import java.io.IOException;
2020
import java.net.InetSocketAddress;
@@ -33,8 +33,9 @@
3333
* pipe.
3434
*
3535
* @author Phillip Webb
36+
* @author Scott Frederick
3637
*/
37-
class DockerConnectionSocketFactory implements ConnectionSocketFactory {
38+
class LocalDockerConnectionSocketFactory implements ConnectionSocketFactory {
3839

3940
private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock";
4041

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,21 @@
1414
* limitations under the License.
1515
*/
1616

17-
package org.springframework.boot.buildpack.platform.docker;
17+
package org.springframework.boot.buildpack.platform.docker.httpclient;
1818

1919
import java.net.InetAddress;
2020
import java.net.UnknownHostException;
2121

2222
import org.apache.http.conn.DnsResolver;
2323

2424
/**
25-
* {@link DnsResolver} used by the {@link DockerHttpClientConnectionManager} to ensure
26-
* only the loopback address is used.
25+
* {@link DnsResolver} used by the {@link LocalDockerHttpClientConnectionManager} to
26+
* ensure only the loopback address is used.
2727
*
2828
* @author Phillip Webb
29+
* @author Scott Frederick
2930
*/
30-
class DockerDnsResolver implements DnsResolver {
31+
class LocalDockerDnsResolver implements DnsResolver {
3132

3233
private static final InetAddress[] LOOPBACK = new InetAddress[] { InetAddress.getLoopbackAddress() };
3334

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.buildpack.platform.docker.httpclient;
18+
19+
import org.apache.http.HttpHost;
20+
import org.apache.http.client.HttpClient;
21+
import org.apache.http.impl.client.CloseableHttpClient;
22+
import org.apache.http.impl.client.HttpClientBuilder;
23+
import org.apache.http.impl.client.HttpClients;
24+
25+
import org.springframework.util.Assert;
26+
27+
/**
28+
* A {@link DockerHttpClientConnection} that describes a connection to a local Docker
29+
* host.
30+
*
31+
* @author Scott Frederick
32+
* @since 2.3.0
33+
*/
34+
public class LocalDockerHttpClientConnection implements DockerHttpClientConnection {
35+
36+
private HttpHost httpHost;
37+
38+
private CloseableHttpClient httpClient;
39+
40+
/**
41+
* Indicate that this factory can be used as a default.
42+
* @return {@code true} always
43+
*/
44+
public boolean accept() {
45+
this.httpHost = HttpHost.create("docker://localhost");
46+
47+
HttpClientBuilder builder = HttpClients.custom();
48+
builder.setConnectionManager(new LocalDockerHttpClientConnectionManager());
49+
builder.setSchemePortResolver(new LocalDockerSchemePortResolver());
50+
this.httpClient = builder.build();
51+
52+
return true;
53+
}
54+
55+
/**
56+
* Get an {@link HttpHost} describing a local Docker host connection.
57+
* @return the {@code HttpHost}
58+
*/
59+
@Override
60+
public HttpHost getHttpHost() {
61+
Assert.state(this.httpHost != null, "DockerHttpClientConnection was not properly initialized");
62+
return this.httpHost;
63+
}
64+
65+
/**
66+
* Get an {@link HttpClient} that can be used to communicate with a local Docker host.
67+
* @return the {@code HttpClient}
68+
*/
69+
@Override
70+
public CloseableHttpClient getHttpClient() {
71+
Assert.state(this.httpClient != null, "DockerHttpClientConnection was not properly initialized");
72+
return this.httpClient;
73+
}
74+
75+
}

0 commit comments

Comments
 (0)