From 4dad56a4911d0b0a50f86335db9311ab0e371edf Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 31 Mar 2020 22:57:42 -0700 Subject: [PATCH] Restructure and polish docker code Restructure and polish docker code to fix a package tangle and provide better separation of concerns. --- .../buildpack/platform/build/Builder.java | 4 +- .../buildpack/platform/docker/DockerApi.java | 13 +- .../DelegatingDockerHttpClientConnection.java | 74 -------- .../LocalDockerConnectionSocketFactory.java | 58 ------ .../httpclient/LocalDockerDnsResolver.java | 40 ----- .../LocalDockerHttpClientConnection.java | 75 -------- ...ocalDockerHttpClientConnectionManager.java | 43 ----- .../LocalDockerSchemePortResolver.java | 44 ----- ...EnvironmentDockerHttpClientConnection.java | 167 ------------------ .../docker/ssl/CertificateParser.java | 71 ++++---- .../platform/docker/ssl/KeyStoreFactory.java | 54 +++--- .../platform/docker/ssl/PrivateKeyParser.java | 116 +++++++----- .../docker/ssl/SslContextFactory.java | 72 ++++---- .../DockerEngineException.java} | 6 +- .../docker/{ => transport}/Errors.java | 2 +- .../HttpClientTransport.java} | 50 +++--- .../HttpTransport.java} | 38 +++- .../LocalDockerSchemePortResolver.java | 17 ++ .../transport/LocalHttpClientTransport.java | 142 +++++++++++++++ .../transport/RemoteHttpClientTransport.java | 90 ++++++++++ .../docker/transport/package-info.java | 20 +++ .../Environment.java} | 26 +-- .../platform/system/package-info.java | 20 +++ .../platform/docker/DockerApiTests.java | 64 +++---- ...onmentDockerHttpClientConnectionTests.java | 102 ----------- .../docker/ssl/KeyStoreFactoryTests.java | 5 - .../docker/ssl/SslContextFactoryTests.java | 3 +- .../DockerEngineExceptionTests.java} | 19 +- .../docker/{ => transport}/ErrorsTests.java | 4 +- .../HttpClientTransportTests.java} | 42 ++--- .../docker/transport/HttpTransportTests.java | 46 +++++ .../RemoteHttpClientTransportTests.java | 95 ++++++++++ .../docker/{ => transport}/errors.json | 0 .../gradle/tasks/bundling/BootBuildImage.java | 4 +- 34 files changed, 750 insertions(+), 876 deletions(-) delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/DelegatingDockerHttpClientConnection.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerConnectionSocketFactory.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerDnsResolver.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerHttpClientConnection.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerHttpClientConnectionManager.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerSchemePortResolver.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/RemoteEnvironmentDockerHttpClientConnection.java rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/{DockerException.java => transport/DockerEngineException.java} (90%) rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/{ => transport}/Errors.java (96%) rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/{HttpClientHttp.java => transport/HttpClientTransport.java} (78%) rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/{Http.java => transport/HttpTransport.java} (64%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalDockerSchemePortResolver.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransport.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/package-info.java rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/{docker/httpclient/DockerHttpClientConnection.java => system/Environment.java} (54%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/package-info.java delete mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/httpclient/RemoteEnvironmentDockerHttpClientConnectionTests.java rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/{DockerExceptionTests.java => transport/DockerEngineExceptionTests.java} (78%) rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/{ => transport}/ErrorsTests.java (91%) rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/{HttpClientHttpTests.java => transport/HttpClientTransportTests.java} (87%) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java rename spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/{ => transport}/errors.json (100%) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index fd3a51469e..441d5316aa 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java @@ -21,10 +21,10 @@ import java.util.function.Consumer; import org.springframework.boot.buildpack.platform.build.BuilderMetadata.Stack; import org.springframework.boot.buildpack.platform.docker.DockerApi; -import org.springframework.boot.buildpack.platform.docker.DockerException; import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener; import org.springframework.boot.buildpack.platform.docker.UpdateListener; +import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.util.Assert; @@ -56,7 +56,7 @@ public class Builder { this.docker = docker; } - public void build(BuildRequest request) throws DockerException, IOException { + public void build(BuildRequest request) throws DockerEngineException, IOException { Assert.notNull(request, "Request must not be null"); this.log.start(request); Image builderImage = pullBuilder(request); 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 ca08839e13..8986c06b9e 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 @@ -26,7 +26,8 @@ import java.util.List; import org.apache.http.client.utils.URIBuilder; -import org.springframework.boot.buildpack.platform.docker.Http.Response; +import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport; +import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response; import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; import org.springframework.boot.buildpack.platform.docker.type.ContainerContent; import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; @@ -53,7 +54,7 @@ public class DockerApi { static final String API_VERSION = "v1.24"; - private final Http http; + private final HttpTransport http; private final JsonStream jsonStream; @@ -67,15 +68,15 @@ public class DockerApi { * Create a new {@link DockerApi} instance. */ public DockerApi() { - this(new HttpClientHttp()); + this(HttpTransport.create()); } /** - * Create a new {@link DockerApi} instance backed by a specific {@link HttpClientHttp} + * Create a new {@link DockerApi} instance backed by a specific {@link HttpTransport} * implementation. * @param http the http implementation */ - DockerApi(Http http) { + DockerApi(HttpTransport http) { this.http = http; this.jsonStream = new JsonStream(SharedObjectMapper.get()); this.image = new ImageApi(); @@ -83,7 +84,7 @@ public class DockerApi { this.volume = new VolumeApi(); } - private Http http() { + private HttpTransport http() { return this.http; } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/DelegatingDockerHttpClientConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/DelegatingDockerHttpClientConnection.java deleted file mode 100644 index 228a90a395..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/DelegatingDockerHttpClientConnection.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.httpclient; - -import org.apache.http.HttpHost; -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.CloseableHttpClient; - -/** - * A {@code DockerHttpClientConnection} that determines an appropriate connection to a - * Docker host by detecting whether a remote Docker host is configured or if a default - * local connection should be used. - * - * @author Scott Frederick - * @since 2.3.0 - */ -public final class DelegatingDockerHttpClientConnection implements DockerHttpClientConnection { - - private static final RemoteEnvironmentDockerHttpClientConnection REMOTE_FACTORY = new RemoteEnvironmentDockerHttpClientConnection(); - - private static final LocalDockerHttpClientConnection LOCAL_FACTORY = new LocalDockerHttpClientConnection(); - - private final DockerHttpClientConnection delegate; - - private DelegatingDockerHttpClientConnection(DockerHttpClientConnection delegate) { - this.delegate = delegate; - } - - /** - * Get an {@link HttpHost} describing the Docker host connection. - * @return the {@code HttpHost} - */ - public HttpHost getHttpHost() { - return this.delegate.getHttpHost(); - } - - /** - * Get an {@link HttpClient} that can be used to communicate with the Docker host. - * @return the {@code HttpClient} - */ - public CloseableHttpClient getHttpClient() { - return this.delegate.getHttpClient(); - } - - /** - * Create a {@link DockerHttpClientConnection} by detecting the connection - * configuration. - * @return the {@code DockerHttpClientConnection} - */ - public static DockerHttpClientConnection create() { - if (REMOTE_FACTORY.accept()) { - return new DelegatingDockerHttpClientConnection(REMOTE_FACTORY); - } - if (LOCAL_FACTORY.accept()) { - return new DelegatingDockerHttpClientConnection(LOCAL_FACTORY); - } - return null; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerConnectionSocketFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerConnectionSocketFactory.java deleted file mode 100644 index 377bff90fb..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerConnectionSocketFactory.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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.httpclient; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.Socket; - -import com.sun.jna.Platform; -import org.apache.http.HttpHost; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.protocol.HttpContext; - -import org.springframework.boot.buildpack.platform.socket.DomainSocket; -import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket; - -/** - * {@link ConnectionSocketFactory} that connects to the Docker domain socket or named - * pipe. - * - * @author Phillip Webb - * @author Scott Frederick - */ -class LocalDockerConnectionSocketFactory implements ConnectionSocketFactory { - - private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock"; - - private static final String WINDOWS_NAMED_PIPE_PATH = "//./pipe/docker_engine"; - - @Override - public Socket createSocket(HttpContext context) throws IOException { - if (Platform.isWindows()) { - return NamedPipeSocket.get(WINDOWS_NAMED_PIPE_PATH); - } - return DomainSocket.get(DOMAIN_SOCKET_PATH); - } - - @Override - public Socket connectSocket(int connectTimeout, Socket sock, HttpHost host, InetSocketAddress remoteAddress, - InetSocketAddress localAddress, HttpContext context) throws IOException { - return sock; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerDnsResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerDnsResolver.java deleted file mode 100644 index c2b7ad23a6..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerDnsResolver.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.httpclient; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -import org.apache.http.conn.DnsResolver; - -/** - * {@link DnsResolver} used by the {@link LocalDockerHttpClientConnectionManager} to - * ensure only the loopback address is used. - * - * @author Phillip Webb - * @author Scott Frederick - */ -class LocalDockerDnsResolver implements DnsResolver { - - private static final InetAddress[] LOOPBACK = new InetAddress[] { InetAddress.getLoopbackAddress() }; - - @Override - public InetAddress[] resolve(String host) throws UnknownHostException { - return LOOPBACK; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerHttpClientConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerHttpClientConnection.java deleted file mode 100644 index 1e02c9e0ad..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerHttpClientConnection.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.httpclient; - -import org.apache.http.HttpHost; -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.HttpClients; - -import org.springframework.util.Assert; - -/** - * A {@link DockerHttpClientConnection} that describes a connection to a local Docker - * host. - * - * @author Scott Frederick - * @since 2.3.0 - */ -public class LocalDockerHttpClientConnection implements DockerHttpClientConnection { - - private HttpHost httpHost; - - private CloseableHttpClient httpClient; - - /** - * Indicate that this factory can be used as a default. - * @return {@code true} always - */ - public boolean accept() { - this.httpHost = HttpHost.create("docker://localhost"); - - HttpClientBuilder builder = HttpClients.custom(); - builder.setConnectionManager(new LocalDockerHttpClientConnectionManager()); - builder.setSchemePortResolver(new LocalDockerSchemePortResolver()); - this.httpClient = builder.build(); - - return true; - } - - /** - * Get an {@link HttpHost} describing a local Docker host connection. - * @return the {@code HttpHost} - */ - @Override - public HttpHost getHttpHost() { - Assert.state(this.httpHost != null, "DockerHttpClientConnection was not properly initialized"); - return this.httpHost; - } - - /** - * Get an {@link HttpClient} that can be used to communicate with a local Docker host. - * @return the {@code HttpClient} - */ - @Override - public CloseableHttpClient getHttpClient() { - Assert.state(this.httpClient != null, "DockerHttpClientConnection was not properly initialized"); - return this.httpClient; - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerHttpClientConnectionManager.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerHttpClientConnectionManager.java deleted file mode 100644 index 693c0e1353..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerHttpClientConnectionManager.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.httpclient; - -import org.apache.http.config.Registry; -import org.apache.http.config.RegistryBuilder; -import org.apache.http.conn.HttpClientConnectionManager; -import org.apache.http.conn.socket.ConnectionSocketFactory; -import org.apache.http.impl.conn.BasicHttpClientConnectionManager; - -/** - * {@link HttpClientConnectionManager} for Docker. - * - * @author Phillip Webb - * @author Scott Frederick - */ -class LocalDockerHttpClientConnectionManager extends BasicHttpClientConnectionManager { - - LocalDockerHttpClientConnectionManager() { - super(getRegistry(), null, null, new LocalDockerDnsResolver()); - } - - private static Registry getRegistry() { - RegistryBuilder builder = RegistryBuilder.create(); - builder.register("docker", new LocalDockerConnectionSocketFactory()); - return builder.build(); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerSchemePortResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerSchemePortResolver.java deleted file mode 100644 index 526a6c330c..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/LocalDockerSchemePortResolver.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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.httpclient; - -import org.apache.http.HttpHost; -import org.apache.http.conn.SchemePortResolver; -import org.apache.http.conn.UnsupportedSchemeException; -import org.apache.http.util.Args; - -/** - * {@link SchemePortResolver} for Docker. - * - * @author Phillip Webb - * @author Scott Frederick - */ -class LocalDockerSchemePortResolver implements SchemePortResolver { - - private static final int DEFAULT_DOCKER_PORT = 2376; - - @Override - public int resolve(HttpHost host) throws UnsupportedSchemeException { - Args.notNull(host, "HTTP host"); - String name = host.getSchemeName(); - if ("docker".equals(name)) { - return DEFAULT_DOCKER_PORT; - } - throw new UnsupportedSchemeException(name + " protocol is not supported"); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/RemoteEnvironmentDockerHttpClientConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/RemoteEnvironmentDockerHttpClientConnection.java deleted file mode 100644 index 004e0e32ee..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/RemoteEnvironmentDockerHttpClientConnection.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * 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.httpclient; - -import javax.net.ssl.SSLContext; - -import org.apache.http.HttpHost; -import org.apache.http.client.HttpClient; -import org.apache.http.conn.ssl.SSLConnectionSocketFactory; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.impl.client.HttpClients; - -import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory; -import org.springframework.util.Assert; - -/** - * A {@link DockerHttpClientConnection} that describes a connection to a remote Docker - * host specified by environment variables. - * - * This implementation looks for the following environment variables: - * - *

- *

    - *
  • {@code DOCKER_HOST} - the URL to a Docker daemon host, such as - * {@code tcp://localhost:2376}
  • - *
  • {@code DOCKER_TLS_VERIFY} - set to {@code 1} to enable secure connection to the - * Docker host via TLS (optional)
  • - *
  • {@code DOCKER_CERT_PATH} - the path to certificate and key files needed for TLS - * verification (required if {@code DOCKER_TLS_VERIFY=1})
  • - *
- * - * @author Scott Frederick - * @since 2.3.0 - */ -public class RemoteEnvironmentDockerHttpClientConnection implements DockerHttpClientConnection { - - private static final String DOCKER_HOST_KEY = "DOCKER_HOST"; - - private static final String DOCKER_TLS_VERIFY_KEY = "DOCKER_TLS_VERIFY"; - - private static final String DOCKER_CERT_PATH_KEY = "DOCKER_CERT_PATH"; - - private final EnvironmentAccessor environment; - - private final SslContextFactory sslContextFactory; - - private HttpHost httpHost; - - private CloseableHttpClient httpClient; - - RemoteEnvironmentDockerHttpClientConnection() { - this.environment = new SystemEnvironmentAccessor(); - this.sslContextFactory = new SslContextFactory(); - } - - RemoteEnvironmentDockerHttpClientConnection(EnvironmentAccessor environmentAccessor, - SslContextFactory sslContextFactory) { - this.environment = environmentAccessor; - this.sslContextFactory = sslContextFactory; - } - - /** - * Indicate whether this factory can create be used to create a connection. - * @return {@code true} if the environment variable {@code DOCKER_HOST} is set, - * {@code false} otherwise - */ - public boolean accept() { - if (this.environment.getProperty("DOCKER_HOST") != null) { - initHttpHost(); - initHttpClient(); - return true; - } - return false; - } - - /** - * Get an {@link HttpHost} from the Docker host specified in the environment. - * @return the {@code HttpHost} - */ - @Override - public HttpHost getHttpHost() { - Assert.state(this.httpHost != null, "DockerHttpClientConnection was not properly initialized"); - return this.httpHost; - } - - /** - * Get an {@link HttpClient} from the Docker connection information specified in the - * environment. - * @return the {@code HttpClient} - */ - @Override - public CloseableHttpClient getHttpClient() { - Assert.state(this.httpClient != null, "DockerHttpClientConnection was not properly initialized"); - return this.httpClient; - } - - private void initHttpHost() { - String dockerHost = this.environment.getProperty(DOCKER_HOST_KEY); - Assert.hasText(dockerHost, "DOCKER_HOST must be set"); - - this.httpHost = HttpHost.create(dockerHost); - if ("tcp".equals(this.httpHost.getSchemeName())) { - String scheme = (isSecure()) ? "https" : "http"; - this.httpHost = new HttpHost(this.httpHost.getHostName(), this.httpHost.getPort(), scheme); - } - } - - private void initHttpClient() { - HttpClientBuilder builder = HttpClients.custom(); - - if (isSecure()) { - String certPath = this.environment.getProperty(DOCKER_CERT_PATH_KEY); - Assert.hasText(certPath, DOCKER_TLS_VERIFY_KEY + " requires trust material location to be specified with " - + DOCKER_CERT_PATH_KEY); - - SSLContext sslContext = this.sslContextFactory.forPath(certPath); - SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext); - - builder.setSSLSocketFactory(sslSocketFactory).setSSLContext(sslContext); - } - - this.httpClient = builder.build(); - } - - private boolean isSecure() { - String tlsVerify = this.environment.getProperty(DOCKER_TLS_VERIFY_KEY); - if (tlsVerify != null) { - try { - return Integer.parseInt(tlsVerify) == 1; - } - catch (NumberFormatException ex) { - return false; - } - } - return false; - } - - interface EnvironmentAccessor { - - String getProperty(String key); - - } - - public static class SystemEnvironmentAccessor implements EnvironmentAccessor { - - public String getProperty(String key) { - return System.getenv(key); - } - - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/CertificateParser.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/CertificateParser.java index c4c55852da..307d722fa3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/CertificateParser.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/CertificateParser.java @@ -26,6 +26,7 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -35,60 +36,70 @@ import org.springframework.util.Base64Utils; * Parser for X.509 certificates in PEM format. * * @author Scott Frederick + * @author Phillip Webb */ final class CertificateParser { - private static final Pattern CERTIFICATE_PATTERN = Pattern - .compile("-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+" + // Header - "([a-z0-9+/=\\r\\n]+)" + // Base64 text - "-+END\\s+.*CERTIFICATE[^-]*-+", // Footer - Pattern.CASE_INSENSITIVE); + private static final String HEADER = "-+BEGIN\\s+.*CERTIFICATE[^-]*-+(?:\\s|\\r|\\n)+"; + + private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)"; + + private static final String FOOTER = "-+END\\s+.*CERTIFICATE[^-]*-+"; + + private static final Pattern PATTERN = Pattern.compile(HEADER + BASE64_TEXT + FOOTER, Pattern.CASE_INSENSITIVE); private CertificateParser() { } /** * Load certificates from the specified file paths. - * @param certPaths one or more paths to certificate files + * @param paths one or more paths to certificate files * @return certificates parsed from specified file paths */ - static X509Certificate[] parse(Path... certPaths) { - List certs = new ArrayList<>(); - for (Path certFile : certPaths) { - certs.addAll(generateCertificates(certFile)); + static X509Certificate[] parse(Path... paths) { + CertificateFactory factory = getCertificateFactory(); + List certificates = new ArrayList<>(); + for (Path path : paths) { + readCertificates(path, factory, certificates::add); } - return certs.toArray(new X509Certificate[0]); + return certificates.toArray(new X509Certificate[0]); } - private static List generateCertificates(Path certPath) { + private static CertificateFactory getCertificateFactory() { try { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - List certs = new ArrayList<>(); - - byte[] certBytes = Files.readAllBytes(certPath); - String certString = new String(certBytes, StandardCharsets.UTF_8); - - Matcher matcher = CERTIFICATE_PATTERN.matcher(certString); + return CertificateFactory.getInstance("X.509"); + } + catch (CertificateException ex) { + throw new IllegalStateException("Unable to get X.509 certificate factory", ex); + } + } + private static void readCertificates(Path path, CertificateFactory factory, Consumer consumer) { + try { + String text = readText(path); + Matcher matcher = PATTERN.matcher(text); while (matcher.find()) { - byte[] content = decodeContent(matcher.group(1)); - ByteArrayInputStream contentStream = new ByteArrayInputStream(content); - while (contentStream.available() > 0) { - certs.add((X509Certificate) certificateFactory.generateCertificate(contentStream)); + String encodedText = matcher.group(1); + byte[] decodedBytes = decodeBase64(encodedText); + ByteArrayInputStream inputStream = new ByteArrayInputStream(decodedBytes); + while (inputStream.available() > 0) { + consumer.accept((X509Certificate) factory.generateCertificate(inputStream)); } } - - return certs; } catch (CertificateException | IOException ex) { - throw new IllegalStateException("Error reading certificate from file " + certPath + ": " + ex.getMessage(), - ex); + throw new IllegalStateException("Error reading certificate from '" + path + "' : " + ex.getMessage(), ex); } } - private static byte[] decodeContent(String content) { - byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes(); - return Base64Utils.decode(contentBytes); + private static String readText(Path path) throws IOException { + byte[] bytes = Files.readAllBytes(path); + return new String(bytes, StandardCharsets.UTF_8); + } + + private static byte[] decodeBase64(String content) { + byte[] bytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes(); + return Base64Utils.decode(bytes); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactory.java index 73b1f1ccf9..f54ac5cbfe 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactory.java @@ -22,7 +22,9 @@ import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; /** @@ -32,6 +34,8 @@ import java.security.cert.X509Certificate; */ final class KeyStoreFactory { + private static final char[] NO_PASSWORD = {}; + private KeyStoreFactory() { } @@ -45,19 +49,15 @@ final class KeyStoreFactory { */ static KeyStore create(Path certPath, Path keyPath, String alias) { try { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - keyStore.load(null); - + KeyStore keyStore = getKeyStore(); X509Certificate[] certificates = CertificateParser.parse(certPath); - - if (keyPath != null && Files.exists(keyPath)) { - PrivateKey privateKey = PrivateKeyParser.parse(keyPath); - addCertsToStore(keyStore, certificates, privateKey, alias); + PrivateKey privateKey = getPrivateKey(keyPath); + try { + addCertificates(keyStore, certificates, privateKey, alias); } - else { - addCertsToStore(keyStore, certificates, alias); + catch (KeyStoreException ex) { + throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex); } - return keyStore; } catch (GeneralSecurityException | IOException ex) { @@ -65,25 +65,29 @@ final class KeyStoreFactory { } } - private static void addCertsToStore(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey, - String alias) { - try { - keyStore.setKeyEntry(alias, privateKey, new char[] {}, certificates); - } - catch (KeyStoreException ex) { - throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex); + private static KeyStore getKeyStore() + throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(null); + return keyStore; + } + + private static PrivateKey getPrivateKey(Path path) { + if (path != null && Files.exists(path)) { + return PrivateKeyParser.parse(path); } + return null; } - private static void addCertsToStore(KeyStore keyStore, X509Certificate[] certs, String alias) { - try { - for (int index = 0; index < certs.length; index++) { - String indexedAlias = alias + "-" + index; - keyStore.setCertificateEntry(indexedAlias, certs[index]); - } + private static void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey, + String alias) throws KeyStoreException { + if (privateKey != null) { + keyStore.setKeyEntry(alias, privateKey, NO_PASSWORD, certificates); } - catch (KeyStoreException ex) { - throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex); + else { + for (int index = 0; index < certificates.length; index++) { + keyStore.setCertificateEntry(alias + "-" + index, certificates[index]); + } } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParser.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParser.java index a4e1c8eaf3..63e870b28d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParser.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/PrivateKeyParser.java @@ -16,6 +16,7 @@ package org.springframework.boot.buildpack.platform.docker.ssl; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -34,62 +35,93 @@ import org.springframework.util.Base64Utils; * Parser for PKCS private key files in PEM format. * * @author Scott Frederick + * @author Phillip Webb */ final class PrivateKeyParser { - private static final Pattern PKCS_1_KEY_PATTERN = Pattern - .compile("-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header - "([a-z0-9+/=\\r\\n]+)" + // Base64 text - "-+END\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+", // Footer - Pattern.CASE_INSENSITIVE); + private static final String PKCS1_HEADER = "-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; - private static final Pattern PKCS_8_KEY_PATTERN = Pattern - .compile("-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header - "([a-z0-9+/=\\r\\n]+)" + // Base64 text - "-+END\\s+PRIVATE\\s+KEY[^-]*-+", // Footer - Pattern.CASE_INSENSITIVE); + private static final String PKCS1_FOOTER = "-+END\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+"; + + private static final String PKCS8_FOOTER = "-+END\\s+PRIVATE\\s+KEY[^-]*-+"; + + private static final String PKCS8_HEADER = "-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+"; + + private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)"; + + private static final Pattern PKCS1_PATTERN = Pattern.compile(PKCS1_HEADER + BASE64_TEXT + PKCS1_FOOTER, + Pattern.CASE_INSENSITIVE); + + private static final Pattern PKCS8_KEY_PATTERN = Pattern.compile(PKCS8_HEADER + BASE64_TEXT + PKCS8_FOOTER, + Pattern.CASE_INSENSITIVE); private PrivateKeyParser() { } /** * Load a private key from the specified file paths. - * @param keyPath the path to the private key file + * @param path the path to the private key file * @return private key from specified file path */ - static PrivateKey parse(Path keyPath) { + static PrivateKey parse(Path path) { try { - byte[] keyBytes = Files.readAllBytes(keyPath); - String keyString = new String(keyBytes, StandardCharsets.UTF_8); - - Matcher matcher = PKCS_1_KEY_PATTERN.matcher(keyString); + String text = readText(path); + Matcher matcher = PKCS1_PATTERN.matcher(text); if (matcher.find()) { - return parsePkcs1PrivateKey(decodeContent(matcher.group(1))); + return parsePkcs1(decodeBase64(matcher.group(1))); } - - matcher = PKCS_8_KEY_PATTERN.matcher(keyString); + matcher = PKCS8_KEY_PATTERN.matcher(text); if (matcher.find()) { - return parsePkcs8PrivateKey(decodeContent(matcher.group(1))); + return parsePkcs8(decodeBase64(matcher.group(1))); } - - throw new IllegalStateException("Unrecognized private key format in " + keyPath); + throw new IllegalStateException("Unrecognized private key format in " + path); } catch (GeneralSecurityException | IOException ex) { - throw new IllegalStateException("Error loading private key file " + keyPath, ex); + throw new IllegalStateException("Error loading private key file " + path, ex); } } - private static byte[] decodeContent(String content) { - byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes(); - return Base64Utils.decode(contentBytes); + private static PrivateKey parsePkcs1(byte[] privateKeyBytes) throws GeneralSecurityException { + byte[] pkcs8Bytes = convertPkcs1ToPkcs8(privateKeyBytes); + return parsePkcs8(pkcs8Bytes); } - private static PrivateKey parsePkcs1PrivateKey(byte[] privateKeyBytes) throws GeneralSecurityException { - byte[] pkcs8Bytes = convertPkcs1ToPkcs8(privateKeyBytes); - return parsePkcs8PrivateKey(pkcs8Bytes); + private static byte[] convertPkcs1ToPkcs8(byte[] pkcs1) { + try { + ByteArrayOutputStream result = new ByteArrayOutputStream(); + int pkcs1Length = pkcs1.length; + int totalLength = pkcs1Length + 22; + // Sequence + total length + result.write(bytes(0x30, 0x82)); + result.write((totalLength >> 8) & 0xff); + result.write(totalLength & 0xff); + // Integer (0) + result.write(bytes(0x02, 0x01, 0x00)); + // Sequence: 1.2.840.113549.1.1.1, NULL + result.write( + bytes(0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00)); + // Octet string + length + result.write(bytes(0x04, 0x82)); + result.write((pkcs1Length >> 8) & 0xff); + result.write(pkcs1Length & 0xff); + // PKCS1 + result.write(pkcs1); + return result.toByteArray(); + } + catch (IOException ex) { + throw new IllegalStateException(ex); + } } - private static PrivateKey parsePkcs8PrivateKey(byte[] privateKeyBytes) throws GeneralSecurityException { + private static byte[] bytes(int... elements) { + byte[] result = new byte[elements.length]; + for (int i = 0; i < elements.length; i++) { + result[i] = (byte) elements[i]; + } + return result; + } + + private static PrivateKey parsePkcs8(byte[] privateKeyBytes) throws GeneralSecurityException { try { PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); @@ -100,26 +132,14 @@ final class PrivateKeyParser { } } - private static byte[] convertPkcs1ToPkcs8(byte[] privateKeyBytes) { - int pkcs1Length = privateKeyBytes.length; - int totalLength = pkcs1Length + 22; - byte[] pkcs8Header = new byte[] { 0x30, (byte) 0x82, (byte) ((totalLength >> 8) & 0xff), - // Sequence + total length - (byte) (totalLength & 0xff), - // Integer (0) - 0x2, 0x1, 0x0, - // Sequence: 1.2.840.113549.1.1.1, NULL - 0x30, 0xD, 0x6, 0x9, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0, - // Octet string + length - 0x4, (byte) 0x82, (byte) ((pkcs1Length >> 8) & 0xff), (byte) (pkcs1Length & 0xff) }; - return join(pkcs8Header, privateKeyBytes); + private static String readText(Path path) throws IOException { + byte[] bytes = Files.readAllBytes(path); + return new String(bytes, StandardCharsets.UTF_8); } - private static byte[] join(byte[] byteArray1, byte[] byteArray2) { - byte[] bytes = new byte[byteArray1.length + byteArray2.length]; - System.arraycopy(byteArray1, 0, bytes, 0, byteArray1.length); - System.arraycopy(byteArray2, 0, bytes, byteArray1.length, byteArray2.length); - return bytes; + private static byte[] decodeBase64(String content) { + byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes(); + return Base64Utils.decode(contentBytes); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactory.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactory.java index a41701e713..1c8349887e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactory.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactory.java @@ -20,71 +20,77 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; +import org.springframework.util.Assert; + /** * Builds an {@link SSLContext} for use with an HTTP connection. * * @author Scott Frederick + * @author Phillip Webb * @since 2.3.0 */ public class SslContextFactory { + private static final char[] NO_PASSWORD = {}; + private static final String KEY_STORE_ALIAS = "spring-boot-docker"; public SslContextFactory() { } /** - * Create an {@link SSLContext} from files in the specified directory. - * - * The directory must contain files with the names 'key.pem', 'cert.pem', and - * 'ca.pem'. - * @param certificatePath the path to a directory containing certificate and key files + * Create an {@link SSLContext} from files in the specified directory. The directory + * must contain files with the names 'key.pem', 'cert.pem', and 'ca.pem'. + * @param directory the path to a directory containing certificate and key files * @return the {@code SSLContext} */ - public SSLContext forPath(String certificatePath) { - + public SSLContext forDirectory(String directory) { try { - - Path keyPath = Paths.get(certificatePath, "key.pem"); - Path certPath = Paths.get(certificatePath, "cert.pem"); - Path certAuthorityPath = Paths.get(certificatePath, "ca.pem"); - Path certAuthorityKeyPath = Paths.get(certificatePath, "ca-key.pem"); - - verifyCertificateFiles(keyPath, certPath, certAuthorityPath); - - KeyStore keyStore = KeyStoreFactory.create(certPath, keyPath, KEY_STORE_ALIAS); - KeyManagerFactory keyManagerFactory = KeyManagerFactory - .getInstance(KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keyStore, new char[] {}); - - KeyStore trustStore = KeyStoreFactory.create(certAuthorityPath, certAuthorityKeyPath, KEY_STORE_ALIAS); - TrustManagerFactory trustManagerFactory = TrustManagerFactory - .getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(trustStore); - + Path keyPath = Paths.get(directory, "key.pem"); + Path certPath = Paths.get(directory, "cert.pem"); + Path caPath = Paths.get(directory, "ca.pem"); + Path caKeyPath = Paths.get(directory, "ca-key.pem"); + verifyCertificateFiles(keyPath, certPath, caPath); + KeyManagerFactory keyManagerFactory = getKeyManagerFactory(keyPath, certPath); + TrustManagerFactory trustManagerFactory = getTrustManagerFactory(caPath, caKeyPath); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null); - return sslContext; - + } + catch (RuntimeException ex) { + throw ex; } catch (Exception ex) { throw new RuntimeException(ex.getMessage(), ex); } + } + + private KeyManagerFactory getKeyManagerFactory(Path keyPath, Path certPath) throws Exception { + KeyStore store = KeyStoreFactory.create(certPath, keyPath, KEY_STORE_ALIAS); + KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + factory.init(store, NO_PASSWORD); + return factory; + } + private TrustManagerFactory getTrustManagerFactory(Path caPath, Path caKeyPath) + throws NoSuchAlgorithmException, KeyStoreException { + KeyStore store = KeyStoreFactory.create(caPath, caKeyPath, KEY_STORE_ALIAS); + TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + factory.init(store); + return factory; } - private static void verifyCertificateFiles(Path... certificateFilePaths) { - for (Path path : certificateFilePaths) { - if (!Files.exists(path)) { - throw new RuntimeException( - "Certificate path must contain the files 'ca.pem', 'cert.pem', and 'key.pem'"); - } + private static void verifyCertificateFiles(Path... paths) { + for (Path path : paths) { + Assert.state(Files.exists(path) && Files.isRegularFile(path), + "Certificate path must contain the files 'ca.pem', 'cert.pem', and 'key.pem' files"); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerException.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java similarity index 90% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerException.java rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java index 8388282eea..853e5cea00 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/DockerException.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.buildpack.platform.docker; +package org.springframework.boot.buildpack.platform.docker.transport; import java.net.URI; @@ -27,7 +27,7 @@ import org.springframework.util.Assert; * @author Scott Frederick * @since 2.3.0 */ -public class DockerException extends RuntimeException { +public class DockerEngineException extends RuntimeException { private final int statusCode; @@ -35,7 +35,7 @@ public class DockerException extends RuntimeException { private final Errors errors; - DockerException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors) { + DockerEngineException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors) { super(buildMessage(host, uri, statusCode, reasonPhrase, errors)); this.statusCode = statusCode; this.reasonPhrase = reasonPhrase; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/Errors.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Errors.java similarity index 96% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/Errors.java rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Errors.java index 183e925979..4aec87719c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/Errors.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/Errors.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.buildpack.platform.docker; +package org.springframework.boot.buildpack.platform.docker.transport; import java.util.Collections; import java.util.Iterator; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/HttpClientHttp.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java similarity index 78% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/HttpClientHttp.java rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java index 843a6a4175..7e52d90a82 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/HttpClientHttp.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransport.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.buildpack.platform.docker; +package org.springframework.boot.buildpack.platform.docker.transport; import java.io.IOException; import java.io.InputStream; @@ -36,29 +36,30 @@ 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.httpclient.DelegatingDockerHttpClientConnection; -import org.springframework.boot.buildpack.platform.docker.httpclient.DockerHttpClientConnection; import org.springframework.boot.buildpack.platform.io.Content; import org.springframework.boot.buildpack.platform.io.IOConsumer; import org.springframework.boot.buildpack.platform.json.SharedObjectMapper; +import org.springframework.util.Assert; /** - * {@link Http} implementation backed by a {@link HttpClient}. + * Abstract base class for {@link HttpTransport} implementations backed by a + * {@link HttpClient}. * * @author Phillip Webb * @author Mike Smithson * @author Scott Frederick */ -class HttpClientHttp implements Http { +abstract class HttpClientTransport implements HttpTransport { - private final DockerHttpClientConnection clientConnection; + private final CloseableHttpClient client; - HttpClientHttp() { - this.clientConnection = DelegatingDockerHttpClientConnection.create(); - } + private final HttpHost host; - HttpClientHttp(DockerHttpClientConnection clientConnection) { - this.clientConnection = clientConnection; + protected HttpClientTransport(CloseableHttpClient client, HttpHost host) { + Assert.notNull(client, "Client must not be null"); + Assert.notNull(host, "Host must not be null"); + this.client = client; + this.host = host; } /** @@ -123,27 +124,20 @@ class HttpClientHttp implements Http { } private Response execute(HttpUriRequest request) { - HttpHost host = this.clientConnection.getHttpHost(); - CloseableHttpClient client = this.clientConnection.getHttpClient(); - try { - CloseableHttpResponse response = client.execute(host, request); + CloseableHttpResponse response = this.client.execute(this.host, request); StatusLine statusLine = response.getStatusLine(); int statusCode = statusLine.getStatusCode(); HttpEntity entity = response.getEntity(); - - if (statusCode >= 400 && statusCode < 500) { - throw new DockerException(host.toHostString(), request.getURI(), statusCode, - statusLine.getReasonPhrase(), getErrorsFromResponse(entity)); - } - if (statusCode == 500) { - throw new DockerException(host.toHostString(), request.getURI(), statusCode, - statusLine.getReasonPhrase(), null); + if (statusCode >= 400 && statusCode <= 500) { + Errors errors = (statusCode != 500) ? getErrorsFromResponse(entity) : null; + throw new DockerEngineException(this.host.toHostString(), request.getURI(), statusCode, + statusLine.getReasonPhrase(), errors); } return new HttpClientResponse(response); } - catch (IOException ioe) { - throw new DockerException(host.toHostString(), request.getURI(), 500, ioe.getMessage(), null); + catch (IOException ex) { + throw new DockerEngineException(this.host.toHostString(), request.getURI(), 500, ex.getMessage(), null); } } @@ -151,11 +145,15 @@ class HttpClientHttp implements Http { try { return SharedObjectMapper.get().readValue(entity.getContent(), Errors.class); } - catch (IOException ioe) { + catch (IOException ex) { return null; } } + HttpHost getHost() { + return this.host; + } + /** * {@link HttpEntity} to send {@link Content} content. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/Http.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java similarity index 64% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/Http.java rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java index b3ae1fa390..94480c2a02 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/Http.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransport.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.buildpack.platform.docker; +package org.springframework.boot.buildpack.platform.docker.transport; import java.io.Closeable; import java.io.IOException; @@ -23,17 +23,19 @@ import java.io.OutputStream; import java.net.URI; import org.springframework.boot.buildpack.platform.io.IOConsumer; +import org.springframework.boot.buildpack.platform.system.Environment; /** - * HTTP transport used by the {@link DockerApi}. + * HTTP transport used for docker access. * * @author Phillip Webb + * @since 2.3.0 */ -interface Http { +public interface HttpTransport { /** * Perform a HTTP GET operation. - * @param uri the destination URI + * @param uri the destination URI (excluding any host/port) * @return the operation response * @throws IOException on IO error */ @@ -41,7 +43,7 @@ interface Http { /** * Perform a HTTP POST operation. - * @param uri the destination URI + * @param uri the destination URI (excluding any host/port) * @return the operation response * @throws IOException on IO error */ @@ -49,7 +51,7 @@ interface Http { /** * Perform a HTTP POST operation. - * @param uri the destination URI + * @param uri the destination URI (excluding any host/port) * @param contentType the content type to write * @param writer a content writer * @return the operation response @@ -59,7 +61,7 @@ interface Http { /** * Perform a HTTP PUT operation. - * @param uri the destination URI + * @param uri the destination URI (excluding any host/port) * @param contentType the content type to write * @param writer a content writer * @return the operation response @@ -69,12 +71,32 @@ interface Http { /** * Perform a HTTP DELETE operation. - * @param uri the destination URI + * @param uri the destination URI (excluding any host/port) * @return the operation response * @throws IOException on IO error */ Response delete(URI uri) throws IOException; + /** + * Create the most suitable {@link HttpTransport} based on the + * {@link Environment#SYSTEM system environment}. + * @return a {@link HttpTransport} instance + */ + static HttpTransport create() { + return create(Environment.SYSTEM); + } + + /** + * Create the most suitable {@link HttpTransport} based on the given + * {@link Environment}. + * @param environment the source environment + * @return a {@link HttpTransport} instance + */ + static HttpTransport create(Environment environment) { + HttpTransport remote = RemoteHttpClientTransport.createIfPossible(environment); + return (remote != null) ? remote : LocalHttpClientTransport.create(); + } + /** * An HTTP operation response. */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalDockerSchemePortResolver.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalDockerSchemePortResolver.java new file mode 100644 index 0000000000..6032d9f74f --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalDockerSchemePortResolver.java @@ -0,0 +1,17 @@ +/* + * 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.transport; 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 new file mode 100644 index 0000000000..3e4e3dd88b --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/LocalHttpClientTransport.java @@ -0,0 +1,142 @@ +/* + * 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.transport; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import com.sun.jna.Platform; +import org.apache.http.HttpHost; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.DnsResolver; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.SchemePortResolver; +import org.apache.http.conn.UnsupportedSchemeException; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.impl.conn.BasicHttpClientConnectionManager; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.Args; + +import org.springframework.boot.buildpack.platform.socket.DomainSocket; +import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket; + +/** + * {@link HttpClientTransport} that talks to local Docker. + * + * @author Phillip Webb + * @author Scott Frederick + */ +final class LocalHttpClientTransport extends HttpClientTransport { + + private static final HttpHost LOCAL_DOCKER_HOST = HttpHost.create("docker://localhost"); + + private LocalHttpClientTransport(CloseableHttpClient client) { + super(client, LOCAL_DOCKER_HOST); + } + + static LocalHttpClientTransport create() { + HttpClientBuilder builder = HttpClients.custom(); + builder.setConnectionManager(new LocalConnectionManager()); + builder.setSchemePortResolver(new LocalSchemePortResolver()); + return new LocalHttpClientTransport(builder.build()); + } + + /** + * {@link HttpClientConnectionManager} for local Docker. + */ + private static class LocalConnectionManager extends BasicHttpClientConnectionManager { + + LocalConnectionManager() { + super(getRegistry(), null, null, new LocalDnsResolver()); + } + + private static Registry getRegistry() { + RegistryBuilder builder = RegistryBuilder.create(); + builder.register("docker", new LocalConnectionSocketFactory()); + return builder.build(); + } + + } + + /** + * {@link DnsResolver} used by the {@link LocalDockerHttpClientConnectionManager} to + * ensure only the loopback address is used. + */ + private static class LocalDnsResolver implements DnsResolver { + + private static final InetAddress[] LOOPBACK = new InetAddress[] { InetAddress.getLoopbackAddress() }; + + @Override + public InetAddress[] resolve(String host) throws UnknownHostException { + return LOOPBACK; + } + + } + + /** + * {@link ConnectionSocketFactory} that connects to the local Docker domain socket or + * named pipe. + */ + private static class LocalConnectionSocketFactory implements ConnectionSocketFactory { + + private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock"; + + private static final String WINDOWS_NAMED_PIPE_PATH = "//./pipe/docker_engine"; + + @Override + public Socket createSocket(HttpContext context) throws IOException { + if (Platform.isWindows()) { + return NamedPipeSocket.get(WINDOWS_NAMED_PIPE_PATH); + } + return DomainSocket.get(DOMAIN_SOCKET_PATH); + } + + @Override + public Socket connectSocket(int connectTimeout, Socket sock, HttpHost host, InetSocketAddress remoteAddress, + InetSocketAddress localAddress, HttpContext context) throws IOException { + return sock; + } + + } + + /** + * {@link SchemePortResolver} for local Docker. + */ + private static class LocalSchemePortResolver implements SchemePortResolver { + + private static final int DEFAULT_DOCKER_PORT = 2376; + + @Override + public int resolve(HttpHost host) throws UnsupportedSchemeException { + Args.notNull(host, "HTTP host"); + String name = host.getSchemeName(); + if ("docker".equals(name)) { + return DEFAULT_DOCKER_PORT; + } + throw new UnsupportedSchemeException(name + " protocol is not supported"); + } + + } + +} 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 new file mode 100644 index 0000000000..6d365a73fa --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransport.java @@ -0,0 +1,90 @@ +/* + * 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.transport; + +import javax.net.ssl.SSLContext; + +import org.apache.http.HttpHost; +import org.apache.http.conn.socket.LayeredConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; + +import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory; +import org.springframework.boot.buildpack.platform.system.Environment; +import org.springframework.util.Assert; + +/** + * {@link HttpClientTransport} that talks to a remote Docker. + * + * @author Scott Frederick + * @author Phillip Webb + */ +final class RemoteHttpClientTransport extends HttpClientTransport { + + private static final String DOCKER_HOST = "DOCKER_HOST"; + + private static final String DOCKER_TLS_VERIFY = "DOCKER_TLS_VERIFY"; + + private static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH"; + + private RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host) { + super(client, host); + } + + static RemoteHttpClientTransport createIfPossible(Environment environment) { + return createIfPossible(environment, new SslContextFactory()); + } + + static RemoteHttpClientTransport createIfPossible(Environment environment, SslContextFactory sslContextFactory) { + String host = environment.get(DOCKER_HOST); + return (host != null) ? create(environment, sslContextFactory, HttpHost.create(host)) : null; + } + + private static RemoteHttpClientTransport create(Environment environment, SslContextFactory sslContextFactory, + HttpHost tcpHost) { + HttpClientBuilder builder = HttpClients.custom(); + boolean secure = isSecure(environment); + if (secure) { + builder.setSSLSocketFactory(getSecureConnectionSocketFactory(environment, sslContextFactory)); + } + String scheme = secure ? "https" : "http"; + HttpHost httpHost = new HttpHost(tcpHost.getHostName(), tcpHost.getPort(), scheme); + return new RemoteHttpClientTransport(builder.build(), httpHost); + } + + private static LayeredConnectionSocketFactory getSecureConnectionSocketFactory(Environment environment, + SslContextFactory sslContextFactory) { + String directory = environment.get(DOCKER_CERT_PATH); + Assert.hasText(directory, + () -> DOCKER_TLS_VERIFY + " requires trust material location to be specified with " + DOCKER_CERT_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); + } + catch (NumberFormatException ex) { + return false; + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/package-info.java new file mode 100644 index 0000000000..60217d2eaf --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/transport/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Docker transport classes providing HTTP operations on a local or remote engine. + */ +package org.springframework.boot.buildpack.platform.docker.transport; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/DockerHttpClientConnection.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/Environment.java similarity index 54% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/DockerHttpClientConnection.java rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/Environment.java index 8f4bf30c90..e687cb6321 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/httpclient/DockerHttpClientConnection.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/Environment.java @@ -14,30 +14,30 @@ * limitations under the License. */ -package org.springframework.boot.buildpack.platform.docker.httpclient; - -import org.apache.http.HttpHost; -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.CloseableHttpClient; +package org.springframework.boot.buildpack.platform.system; /** - * Describes a connection to a Docker host. + * Provides access to environment variable values. * * @author Scott Frederick + * @author Phillip Webb * @since 2.3.0 */ -public interface DockerHttpClientConnection { +@FunctionalInterface +public interface Environment { /** - * Create an {@link HttpHost} describing the Docker host connection. - * @return the {@code HttpHost} + * Standard {@link Environment} implementation backed by + * {@link System#getenv(String)}. */ - HttpHost getHttpHost(); + Environment SYSTEM = System::getenv; /** - * Create an {@link HttpClient} that can be used to communicate with the Docker host. - * @return the {@code HttpClient} + * Gets the value of the specified environment variable. + * @param name the name of the environment variable + * @return the string value of the variable, or {@code null} if the variable is not + * defined in the environment */ - CloseableHttpClient getHttpClient(); + String get(String name); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/package-info.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/package-info.java new file mode 100644 index 0000000000..06fb86716d --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/system/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * System abstractions. + */ +package org.springframework.boot.buildpack.platform.system; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java index 80577d7486..12b10431e3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerApiTests.java @@ -33,7 +33,8 @@ import org.mockito.MockitoAnnotations; import org.springframework.boot.buildpack.platform.docker.DockerApi.ContainerApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.ImageApi; import org.springframework.boot.buildpack.platform.docker.DockerApi.VolumeApi; -import org.springframework.boot.buildpack.platform.docker.Http.Response; +import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport; +import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response; import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; import org.springframework.boot.buildpack.platform.docker.type.ContainerContent; import org.springframework.boot.buildpack.platform.docker.type.ContainerReference; @@ -74,18 +75,18 @@ class DockerApiTests { private static final String VOLUMES_URL = API_URL + "/volumes"; @Mock - private HttpClientHttp httpClient; + private HttpTransport http; private DockerApi dockerApi; @BeforeEach void setup() { MockitoAnnotations.initMocks(this); - this.dockerApi = new DockerApi(this.httpClient); + this.dockerApi = new DockerApi(this.http); } - private HttpClientHttp httpClient() { - return this.httpClient; + private HttpTransport http() { + return this.http; } private Response emptyResponse() { @@ -148,8 +149,8 @@ class DockerApiTests { URI createUri = new URI(IMAGES_URL + "/create?fromImage=docker.io%2Fcloudfoundry%2Fcnb%3Abionic"); String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30"; URI imageUri = new URI(IMAGES_URL + "/docker.io/cloudfoundry/cnb@sha256:" + imageHash + "/json"); - given(httpClient().post(createUri)).willReturn(responseOf("pull-stream.json")); - given(httpClient().get(imageUri)).willReturn(responseOf("type/image.json")); + given(http().post(createUri)).willReturn(responseOf("pull-stream.json")); + given(http().get(imageUri)).willReturn(responseOf("type/image.json")); Image image = this.api.pull(reference, this.pullListener); assertThat(image.getLayers()).hasSize(46); InOrder ordered = inOrder(this.pullListener); @@ -176,14 +177,13 @@ class DockerApiTests { Image image = Image.of(getClass().getResourceAsStream("type/image.json")); ImageArchive archive = ImageArchive.from(image); URI loadUri = new URI(IMAGES_URL + "/load"); - given(httpClient().post(eq(loadUri), eq("application/x-tar"), any())) - .willReturn(responseOf("load-stream.json")); + given(http().post(eq(loadUri), eq("application/x-tar"), any())).willReturn(responseOf("load-stream.json")); this.api.load(archive, this.loadListener); InOrder ordered = inOrder(this.loadListener); ordered.verify(this.loadListener).onStart(); ordered.verify(this.loadListener).onUpdate(any()); ordered.verify(this.loadListener).onFinish(); - verify(httpClient()).post(any(), any(), this.writer.capture()); + verify(http()).post(any(), any(), this.writer.capture()); ByteArrayOutputStream out = new ByteArrayOutputStream(); this.writer.getValue().accept(out); assertThat(out.toByteArray()).hasSizeGreaterThan(21000); @@ -201,9 +201,9 @@ class DockerApiTests { .of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); URI removeUri = new URI(IMAGES_URL + "/docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); - given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + given(http().delete(removeUri)).willReturn(emptyResponse()); this.api.remove(reference, false); - verify(httpClient()).delete(removeUri); + verify(http()).delete(removeUri); } @Test @@ -212,9 +212,9 @@ class DockerApiTests { .of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d"); URI removeUri = new URI(IMAGES_URL + "/docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d?force=1"); - given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + given(http().delete(removeUri)).willReturn(emptyResponse()); this.api.remove(reference, true); - verify(httpClient()).delete(removeUri); + verify(http()).delete(removeUri); } } @@ -247,12 +247,12 @@ class DockerApiTests { ImageReference imageReference = ImageReference.of("ubuntu:bionic"); ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash")); URI createUri = new URI(CONTAINERS_URL + "/create"); - given(httpClient().post(eq(createUri), eq("application/json"), any())) + given(http().post(eq(createUri), eq("application/json"), any())) .willReturn(responseOf("create-container-response.json")); ContainerReference containerReference = this.api.create(config); assertThat(containerReference.toString()).isEqualTo("e90e34656806"); ByteArrayOutputStream out = new ByteArrayOutputStream(); - verify(httpClient()).post(any(), any(), this.writer.capture()); + verify(http()).post(any(), any(), this.writer.capture()); this.writer.getValue().accept(out); assertThat(out.toByteArray()).hasSizeGreaterThan(130); } @@ -267,17 +267,17 @@ class DockerApiTests { }); ContainerContent content = ContainerContent.of(archive); URI createUri = new URI(CONTAINERS_URL + "/create"); - given(httpClient().post(eq(createUri), eq("application/json"), any())) + given(http().post(eq(createUri), eq("application/json"), any())) .willReturn(responseOf("create-container-response.json")); URI uploadUri = new URI(CONTAINERS_URL + "/e90e34656806/archive?path=%2F"); - given(httpClient().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse()); + given(http().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse()); ContainerReference containerReference = this.api.create(config, content); assertThat(containerReference.toString()).isEqualTo("e90e34656806"); ByteArrayOutputStream out = new ByteArrayOutputStream(); - verify(httpClient()).post(any(), any(), this.writer.capture()); + verify(http()).post(any(), any(), this.writer.capture()); this.writer.getValue().accept(out); assertThat(out.toByteArray()).hasSizeGreaterThan(130); - verify(httpClient()).put(any(), any(), this.writer.capture()); + verify(http()).put(any(), any(), this.writer.capture()); this.writer.getValue().accept(out); assertThat(out.toByteArray()).hasSizeGreaterThan(2000); } @@ -292,9 +292,9 @@ class DockerApiTests { void startStartsContainer() throws Exception { ContainerReference reference = ContainerReference.of("e90e34656806"); URI startContainerUri = new URI(CONTAINERS_URL + "/e90e34656806/start"); - given(httpClient().post(startContainerUri)).willReturn(emptyResponse()); + given(http().post(startContainerUri)).willReturn(emptyResponse()); this.api.start(reference); - verify(httpClient()).post(startContainerUri); + verify(http()).post(startContainerUri); } @Test @@ -314,7 +314,7 @@ class DockerApiTests { void logsProducesEvents() throws Exception { ContainerReference reference = ContainerReference.of("e90e34656806"); URI logsUri = new URI(CONTAINERS_URL + "/e90e34656806/logs?stdout=1&stderr=1&follow=1"); - given(httpClient().get(logsUri)).willReturn(responseOf("log-update-event.stream")); + given(http().get(logsUri)).willReturn(responseOf("log-update-event.stream")); this.api.logs(reference, this.logListener); InOrder ordered = inOrder(this.logListener); ordered.verify(this.logListener).onStart(); @@ -332,7 +332,7 @@ class DockerApiTests { void waitReturnsStatus() throws Exception { ContainerReference reference = ContainerReference.of("e90e34656806"); URI waitUri = new URI(CONTAINERS_URL + "/e90e34656806/wait"); - given(httpClient().post(waitUri)).willReturn(responseOf("container-wait-response.json")); + given(http().post(waitUri)).willReturn(responseOf("container-wait-response.json")); ContainerStatus status = this.api.wait(reference); assertThat(status.getStatusCode()).isEqualTo(1); } @@ -347,18 +347,18 @@ class DockerApiTests { void removeRemovesContainer() throws Exception { ContainerReference reference = ContainerReference.of("e90e34656806"); URI removeUri = new URI(CONTAINERS_URL + "/e90e34656806"); - given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + given(http().delete(removeUri)).willReturn(emptyResponse()); this.api.remove(reference, false); - verify(httpClient()).delete(removeUri); + verify(http()).delete(removeUri); } @Test void removeWhenForceIsTrueRemovesContainer() throws Exception { ContainerReference reference = ContainerReference.of("e90e34656806"); URI removeUri = new URI(CONTAINERS_URL + "/e90e34656806?force=1"); - given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + given(http().delete(removeUri)).willReturn(emptyResponse()); this.api.remove(reference, true); - verify(httpClient()).delete(removeUri); + verify(http()).delete(removeUri); } } @@ -390,18 +390,18 @@ class DockerApiTests { void deleteDeletesContainer() throws Exception { VolumeName name = VolumeName.of("test"); URI removeUri = new URI(VOLUMES_URL + "/test"); - given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + given(http().delete(removeUri)).willReturn(emptyResponse()); this.api.delete(name, false); - verify(httpClient()).delete(removeUri); + verify(http()).delete(removeUri); } @Test void deleteWhenForceIsTrueDeletesContainer() throws Exception { VolumeName name = VolumeName.of("test"); URI removeUri = new URI(VOLUMES_URL + "/test?force=1"); - given(httpClient().delete(removeUri)).willReturn(emptyResponse()); + given(http().delete(removeUri)).willReturn(emptyResponse()); this.api.delete(name, true); - verify(httpClient()).delete(removeUri); + verify(http()).delete(removeUri); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/httpclient/RemoteEnvironmentDockerHttpClientConnectionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/httpclient/RemoteEnvironmentDockerHttpClientConnectionTests.java deleted file mode 100644 index bb1ee42c6b..0000000000 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/httpclient/RemoteEnvironmentDockerHttpClientConnectionTests.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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.httpclient; - -import java.security.NoSuchAlgorithmException; - -import javax.net.ssl.SSLContext; - -import org.apache.http.HttpHost; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.boot.buildpack.platform.docker.httpclient.RemoteEnvironmentDockerHttpClientConnection.EnvironmentAccessor; -import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; - -/** - * Tests for {@link RemoteEnvironmentDockerHttpClientConnection}. - * - * @author Scott Frederick - */ -class RemoteEnvironmentDockerHttpClientConnectionTests { - - private EnvironmentAccessor environment; - - private RemoteEnvironmentDockerHttpClientConnection connection; - - private SslContextFactory sslContextFactory; - - @BeforeEach - void setUp() { - this.environment = mock(EnvironmentAccessor.class); - this.sslContextFactory = mock(SslContextFactory.class); - this.connection = new RemoteEnvironmentDockerHttpClientConnection(this.environment, this.sslContextFactory); - } - - @Test - void notAcceptedWhenDockerHostNotSet() { - assertThat(this.connection.accept()).isFalse(); - assertThatIllegalStateException().isThrownBy(() -> this.connection.getHttpHost()); - assertThatIllegalStateException().isThrownBy(() -> this.connection.getHttpClient()); - } - - @Test - void acceptedWhenDockerHostIsSet() { - given(this.environment.getProperty("DOCKER_HOST")).willReturn("tcp://192.168.1.2:2376"); - assertThat(this.connection.accept()).isTrue(); - } - - @Test - void invalidTlsConfigurationThrowsException() { - given(this.environment.getProperty("DOCKER_HOST")).willReturn("tcp://192.168.1.2:2376"); - given(this.environment.getProperty("DOCKER_TLS_VERIFY")).willReturn("1"); - assertThatIllegalArgumentException().isThrownBy(() -> this.connection.accept()) - .withMessageContaining("DOCKER_CERT_PATH"); - } - - @Test - void hostProtocolIsHttpWhenNotSecure() { - given(this.environment.getProperty("DOCKER_HOST")).willReturn("tcp://192.168.1.2:2376"); - assertThat(this.connection.accept()).isTrue(); - HttpHost host = this.connection.getHttpHost(); - assertThat(host).isNotNull(); - assertThat(host.getSchemeName()).isEqualTo("http"); - assertThat(host.getHostName()).isEqualTo("192.168.1.2"); - assertThat(host.getPort()).isEqualTo(2376); - } - - @Test - void hostProtocolIsHttpsWhenSecure() throws NoSuchAlgorithmException { - given(this.environment.getProperty("DOCKER_HOST")).willReturn("tcp://192.168.1.2:2376"); - given(this.environment.getProperty("DOCKER_TLS_VERIFY")).willReturn("1"); - given(this.environment.getProperty("DOCKER_CERT_PATH")).willReturn("/test-cert-path"); - given(this.sslContextFactory.forPath("/test-cert-path")).willReturn(SSLContext.getDefault()); - assertThat(this.connection.accept()).isTrue(); - HttpHost host = this.connection.getHttpHost(); - assertThat(host).isNotNull(); - assertThat(host.getSchemeName()).isEqualTo("https"); - assertThat(host.getHostName()).isEqualTo("192.168.1.2"); - assertThat(host.getPort()).isEqualTo(2376); - } - -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactoryTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactoryTests.java index bc5c9810d0..083f5d9b04 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactoryTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/KeyStoreFactoryTests.java @@ -54,15 +54,12 @@ class KeyStoreFactoryTests { throws IOException, KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException { Path certPath = this.fileWriter.writeFile("cert.pem", PemFileWriter.CA_CERTIFICATE, PemFileWriter.CERTIFICATE); KeyStore keyStore = KeyStoreFactory.create(certPath, null, "test-alias"); - assertThat(keyStore.containsAlias("test-alias-0")).isTrue(); assertThat(keyStore.getCertificate("test-alias-0")).isNotNull(); assertThat(keyStore.getKey("test-alias-0", new char[] {})).isNull(); - assertThat(keyStore.containsAlias("test-alias-1")).isTrue(); assertThat(keyStore.getCertificate("test-alias-1")).isNotNull(); assertThat(keyStore.getKey("test-alias-1", new char[] {})).isNull(); - Files.delete(certPath); } @@ -72,11 +69,9 @@ class KeyStoreFactoryTests { Path certPath = this.fileWriter.writeFile("cert.pem", PemFileWriter.CA_CERTIFICATE, PemFileWriter.CERTIFICATE); Path keyPath = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_KEY); KeyStore keyStore = KeyStoreFactory.create(certPath, keyPath, "test-alias"); - assertThat(keyStore.containsAlias("test-alias")).isTrue(); assertThat(keyStore.getCertificate("test-alias")).isNotNull(); assertThat(keyStore.getKey("test-alias", new char[] {})).isNotNull(); - Files.delete(certPath); Files.delete(keyPath); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactoryTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactoryTests.java index 0d19c47b41..0f370d5a8e 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactoryTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ssl/SslContextFactoryTests.java @@ -50,8 +50,7 @@ class SslContextFactoryTests { this.fileWriter.writeFile("cert.pem", PemFileWriter.CERTIFICATE); this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_KEY); this.fileWriter.writeFile("ca.pem", PemFileWriter.CA_CERTIFICATE); - - SSLContext sslContext = new SslContextFactory().forPath(this.fileWriter.getTempDir().toString()); + SSLContext sslContext = new SslContextFactory().forDirectory(this.fileWriter.getTempDir().toString()); assertThat(sslContext).isNotNull(); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerExceptionTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineExceptionTests.java similarity index 78% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerExceptionTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineExceptionTests.java index 9ebdce22a8..3151bc8dfb 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/DockerExceptionTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/DockerEngineExceptionTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.buildpack.platform.docker; +package org.springframework.boot.buildpack.platform.docker.transport; import java.net.URI; import java.net.URISyntaxException; @@ -26,12 +26,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; /** - * Tests for {@link DockerException}. + * Tests for {@link DockerEngineException}. * * @author Phillip Webb * @author Scott Frederick */ -class DockerExceptionTests { +class DockerEngineExceptionTests { private static final String HOST = "docker://localhost/"; @@ -51,20 +51,21 @@ class DockerExceptionTests { @Test void createWhenHostIsNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> new DockerException(null, null, 404, null, NO_ERRORS)) + assertThatIllegalArgumentException() + .isThrownBy(() -> new DockerEngineException(null, null, 404, null, NO_ERRORS)) .withMessage("host must not be null"); } @Test void createWhenUriIsNullThrowsException() { assertThatIllegalArgumentException() - .isThrownBy(() -> new DockerException(this.HOST, null, 404, null, NO_ERRORS)) + .isThrownBy(() -> new DockerEngineException(HOST, null, 404, null, NO_ERRORS)) .withMessage("URI must not be null"); } @Test void create() { - DockerException exception = new DockerException(HOST, URI, 404, "missing", ERRORS); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", ERRORS); assertThat(exception.getMessage()).isEqualTo( "Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]"); assertThat(exception.getStatusCode()).isEqualTo(404); @@ -74,7 +75,7 @@ class DockerExceptionTests { @Test void createWhenReasonPhraseIsNull() { - DockerException exception = new DockerException(HOST, URI, 404, null, ERRORS); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, null, ERRORS); assertThat(exception.getMessage()).isEqualTo( "Docker API call to 'docker://localhost/example' failed with status code 404 [code: message]"); assertThat(exception.getStatusCode()).isEqualTo(404); @@ -84,13 +85,13 @@ class DockerExceptionTests { @Test void createWhenErrorsIsNull() { - DockerException exception = new DockerException(HOST, URI, 404, "missing", null); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", null); assertThat(exception.getErrors()).isNull(); } @Test void createWhenErrorsIsEmpty() { - DockerException exception = new DockerException(HOST, URI, 404, "missing", NO_ERRORS); + DockerEngineException exception = new DockerEngineException(HOST, URI, 404, "missing", NO_ERRORS); assertThat(exception.getMessage()) .isEqualTo("Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\""); assertThat(exception.getStatusCode()).isEqualTo(404); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ErrorsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ErrorsTests.java similarity index 91% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ErrorsTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ErrorsTests.java index 2cb819336a..705a2ffc94 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/ErrorsTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/ErrorsTests.java @@ -14,13 +14,13 @@ * limitations under the License. */ -package org.springframework.boot.buildpack.platform.docker; +package org.springframework.boot.buildpack.platform.docker.transport; import java.util.Iterator; import org.junit.jupiter.api.Test; -import org.springframework.boot.buildpack.platform.docker.Errors.Error; +import org.springframework.boot.buildpack.platform.docker.transport.Errors.Error; import org.springframework.boot.buildpack.platform.json.AbstractJsonTests; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/HttpClientHttpTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java similarity index 87% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/HttpClientHttpTests.java rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java index 7ed7c12418..eff80e28a8 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/HttpClientHttpTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpClientTransportTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.boot.buildpack.platform.docker; +package org.springframework.boot.buildpack.platform.docker.transport; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -42,8 +42,7 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.springframework.boot.buildpack.platform.docker.Http.Response; -import org.springframework.boot.buildpack.platform.docker.httpclient.DockerHttpClientConnection; +import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response; import org.springframework.util.StreamUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -53,13 +52,13 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.verify; /** - * Tests for {@link HttpClientHttp}. + * Tests for {@link HttpClientTransport}. * * @author Phillip Webb * @author Mike Smithson * @author Scott Frederick */ -class HttpClientHttpTests { +class HttpClientTransportTests { private static final String APPLICATION_JSON = "application/json"; @@ -84,7 +83,7 @@ class HttpClientHttpTests { @Captor private ArgumentCaptor requestCaptor; - private HttpClientHttp http; + private HttpClientTransport http; private URI uri; @@ -94,7 +93,7 @@ class HttpClientHttpTests { given(this.client.execute(any(HttpHost.class), any(HttpRequest.class))).willReturn(this.response); given(this.response.getEntity()).willReturn(this.entity); given(this.response.getStatusLine()).willReturn(this.statusLine); - this.http = new HttpClientHttp(new TestClientConnection(this.client)); + this.http = new TestHttpClientTransport(this.client); this.uri = new URI("example"); } @@ -181,14 +180,14 @@ class HttpClientHttpTests { void executeWhenResposeIsIn400RangeShouldThrowDockerException() throws IOException { given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("errors.json")); given(this.statusLine.getStatusCode()).willReturn(404); - assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri)) + assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) .satisfies((ex) -> assertThat(ex.getErrors()).hasSize(2)); } @Test void executeWhenResposeIsIn500RangeShouldThrowDockerException() { given(this.statusLine.getStatusCode()).willReturn(500); - assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri)) + assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) .satisfies((ex) -> assertThat(ex.getErrors()).isNull()); } @@ -196,8 +195,8 @@ class HttpClientHttpTests { void executeWhenClientThrowsIOExceptionRethrowsAsDockerException() throws IOException { given(this.client.execute(any(HttpHost.class), any(HttpRequest.class))) .willThrow(new IOException("test IO exception")); - assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri)) - .satisfies((ex) -> assertThat(ex.getErrors()).isNull()).satisfies(DockerException::getStatusCode) + assertThatExceptionOfType(DockerEngineException.class).isThrownBy(() -> this.http.get(this.uri)) + .satisfies((ex) -> assertThat(ex.getErrors()).isNull()).satisfies(DockerEngineException::getStatusCode) .withMessageContaining("500") .satisfies((ex) -> assertThat(ex.getReasonPhrase()).contains("test IO exception")); } @@ -208,22 +207,13 @@ class HttpClientHttpTests { return new String(out.toByteArray(), StandardCharsets.UTF_8); } - private static final class TestClientConnection implements DockerHttpClientConnection { + /** + * Test {@link HttpClientTransport} implementation. + */ + static class TestHttpClientTransport extends HttpClientTransport { - private final CloseableHttpClient client; - - private TestClientConnection(CloseableHttpClient client) { - this.client = client; - } - - @Override - public HttpHost getHttpHost() { - return HttpHost.create("docker://localhost"); - } - - @Override - public CloseableHttpClient getHttpClient() { - return this.client; + protected TestHttpClientTransport(CloseableHttpClient client) { + super(client, HttpHost.create("docker://localhost")); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java new file mode 100644 index 0000000000..5821cac946 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/HttpTransportTests.java @@ -0,0 +1,46 @@ +/* + * 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.transport; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HttpTransport}. + * + * @author Phillip Webb + */ +class HttpTransportTests { + + @Test + void createWhenHasDockerHostVariableReturnsRemote() { + Map environment = Collections.singletonMap("DOCKER_HOST", "192.168.1.0"); + HttpTransport transport = HttpTransport.create(environment::get); + assertThat(transport).isInstanceOf(RemoteHttpClientTransport.class); + } + + @Test + void createWhenDoesNotHaveDockerHostVariableReturnsLocal() { + HttpTransport transport = HttpTransport.create((name) -> null); + assertThat(transport).isInstanceOf(LocalHttpClientTransport.class); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/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 new file mode 100644 index 0000000000..8efd3311e5 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/transport/RemoteHttpClientTransportTests.java @@ -0,0 +1,95 @@ +/* + * 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.transport; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Consumer; + +import javax.net.ssl.SSLContext; + +import org.apache.http.HttpHost; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link RemoteHttpClientTransport} + * + * @author Scott Frederick + * @author Phillip Webb + */ +class RemoteHttpClientTransportTests { + + private Map environment = new LinkedHashMap<>(); + + @Test + void createIfPossibleWhenDockerHostIsNotSetReturnsNull() { + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get); + assertThat(transport).isNull(); + } + + @Test + void createIfPossibleWhenDockerHostIsSetReturnsTransport() { + this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get); + assertThat(transport).isNotNull(); + } + + @Test + void createIfPossibleWhenTlsVerifyWithMissingCertPathThrowsException() { + 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)) + .withMessageContaining("DOCKER_CERT_PATH"); + } + + @Test + void createIfPossibleWhenNoTlsVerifyUsesHttp() { + this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get); + assertThat(transport.getHost()).satisfies(hostOf("http", "192.168.1.2", 2376)); + } + + @Test + void createIfPossibleWhenTlsVerifyUsesHttps() 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"); + SslContextFactory sslContextFactory = mock(SslContextFactory.class); + given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault()); + RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, + sslContextFactory); + assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376)); + } + + private Consumer hostOf(String scheme, String hostName, int port) { + return (host) -> { + assertThat(host).isNotNull(); + assertThat(host.getSchemeName()).isEqualTo(scheme); + assertThat(host.getHostName()).isEqualTo(hostName); + assertThat(host.getPort()).isEqualTo(port); + }; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/errors.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/errors.json similarity index 100% rename from spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/errors.json rename to spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/transport/errors.json diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 847361977f..c84426e523 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -34,7 +34,7 @@ import org.gradle.api.tasks.options.Option; import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.Builder; import org.springframework.boot.buildpack.platform.build.Creator; -import org.springframework.boot.buildpack.platform.docker.DockerException; +import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.ImageName; import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.ZipFileTarArchive; @@ -203,7 +203,7 @@ public class BootBuildImage extends DefaultTask { } @TaskAction - void buildImage() throws DockerException, IOException { + void buildImage() throws DockerEngineException, IOException { Builder builder = new Builder(); BuildRequest request = createRequest(); builder.build(request);