mirror of
https://github.com/spring-projects/spring-boot.git
synced 2024-08-29 03:06:45 +08:00
Support remote Docker daemon for building images
Prior to this commit, the build plugin goal/task for building images required a locally running Docker daemon that was accessed via a non-networked socket or pipe. This commit adds support for remote Docker daemons at a location specified by the environment variable `DOCKER_HOST`. Additional environment variables `DOCKER_TLS_VERIFY` and `DOCKER_CERT_PATH` are recognized for configuring a secure TLS connection to the daemon. Fixes gh-20538
This commit is contained in:
parent
fd05bc2a4a
commit
ed6e54218d
@ -44,6 +44,7 @@ import org.springframework.util.StringUtils;
|
||||
* Provides access to the limited set of Docker APIs needed by pack.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class DockerApi {
|
||||
@ -96,7 +97,7 @@ public class DockerApi {
|
||||
|
||||
private URI buildUrl(String path, String... params) {
|
||||
try {
|
||||
URIBuilder builder = new URIBuilder("docker://localhost/" + API_VERSION + path);
|
||||
URIBuilder builder = new URIBuilder("/" + API_VERSION + path);
|
||||
int param = 0;
|
||||
while (param < params.length) {
|
||||
builder.addParameter(params[param++], params[param++]);
|
||||
|
@ -24,6 +24,7 @@ import org.springframework.util.Assert;
|
||||
* Exception throw when the Docker API fails.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class DockerException extends RuntimeException {
|
||||
@ -34,8 +35,8 @@ public class DockerException extends RuntimeException {
|
||||
|
||||
private final Errors errors;
|
||||
|
||||
DockerException(URI uri, int statusCode, String reasonPhrase, Errors errors) {
|
||||
super(buildMessage(uri, statusCode, reasonPhrase, errors));
|
||||
DockerException(String host, URI uri, int statusCode, String reasonPhrase, Errors errors) {
|
||||
super(buildMessage(host, uri, statusCode, reasonPhrase, errors));
|
||||
this.statusCode = statusCode;
|
||||
this.reasonPhrase = reasonPhrase;
|
||||
this.errors = errors;
|
||||
@ -66,10 +67,11 @@ public class DockerException extends RuntimeException {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
private static String buildMessage(URI uri, int statusCode, String reasonPhrase, Errors errors) {
|
||||
private static String buildMessage(String host, URI uri, int statusCode, String reasonPhrase, Errors errors) {
|
||||
Assert.notNull(host, "host must not be null");
|
||||
Assert.notNull(uri, "URI must not be null");
|
||||
StringBuilder message = new StringBuilder(
|
||||
"Docker API call to '" + uri + "' failed with status code " + statusCode);
|
||||
"Docker API call to '" + host + uri + "' failed with status code " + statusCode);
|
||||
if (reasonPhrase != null && !reasonPhrase.isEmpty()) {
|
||||
message.append(" \"" + reasonPhrase + "\"");
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import java.net.URI;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
@ -34,9 +35,9 @@ import org.apache.http.client.methods.HttpPut;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.entity.AbstractHttpEntity;
|
||||
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.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;
|
||||
@ -50,17 +51,14 @@ import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
|
||||
*/
|
||||
class HttpClientHttp implements Http {
|
||||
|
||||
private final CloseableHttpClient client;
|
||||
private final DockerHttpClientConnection clientConnection;
|
||||
|
||||
HttpClientHttp() {
|
||||
HttpClientBuilder builder = HttpClients.custom();
|
||||
builder.setConnectionManager(new DockerHttpClientConnectionManager());
|
||||
builder.setSchemePortResolver(new DockerSchemePortResolver());
|
||||
this.client = builder.build();
|
||||
this.clientConnection = DelegatingDockerHttpClientConnection.create();
|
||||
}
|
||||
|
||||
HttpClientHttp(CloseableHttpClient client) {
|
||||
this.client = client;
|
||||
HttpClientHttp(DockerHttpClientConnection clientConnection) {
|
||||
this.clientConnection = clientConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -90,7 +88,6 @@ class HttpClientHttp implements Http {
|
||||
* @param writer a content writer
|
||||
* @return the operation response
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Response post(URI uri, String contentType, IOConsumer<OutputStream> writer) {
|
||||
return execute(new HttpPost(uri), contentType, writer);
|
||||
@ -103,7 +100,6 @@ class HttpClientHttp implements Http {
|
||||
* @param writer a content writer
|
||||
* @return the operation response
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Response put(URI uri, String contentType, IOConsumer<OutputStream> writer) {
|
||||
return execute(new HttpPut(uri), contentType, writer);
|
||||
@ -114,7 +110,6 @@ class HttpClientHttp implements Http {
|
||||
* @param uri the destination URI
|
||||
* @return the operation response
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Response delete(URI uri) {
|
||||
return execute(new HttpDelete(uri));
|
||||
@ -128,23 +123,36 @@ class HttpClientHttp implements Http {
|
||||
}
|
||||
|
||||
private Response execute(HttpUriRequest request) {
|
||||
HttpHost host = this.clientConnection.getHttpHost();
|
||||
CloseableHttpClient client = this.clientConnection.getHttpClient();
|
||||
|
||||
try {
|
||||
CloseableHttpResponse response = this.client.execute(request);
|
||||
CloseableHttpResponse response = client.execute(host, request);
|
||||
StatusLine statusLine = response.getStatusLine();
|
||||
int statusCode = statusLine.getStatusCode();
|
||||
HttpEntity entity = response.getEntity();
|
||||
|
||||
if (statusCode >= 400 && statusCode < 500) {
|
||||
Errors errors = SharedObjectMapper.get().readValue(entity.getContent(), Errors.class);
|
||||
throw new DockerException(request.getURI(), statusCode, statusLine.getReasonPhrase(), errors);
|
||||
throw new DockerException(host.toHostString(), request.getURI(), statusCode,
|
||||
statusLine.getReasonPhrase(), getErrorsFromResponse(entity));
|
||||
}
|
||||
if (statusCode == 500) {
|
||||
throw new DockerException(request.getURI(), statusCode, statusLine.getReasonPhrase(), null);
|
||||
throw new DockerException(host.toHostString(), request.getURI(), statusCode,
|
||||
statusLine.getReasonPhrase(), null);
|
||||
}
|
||||
return new HttpClientResponse(response);
|
||||
}
|
||||
catch (IOException ioe) {
|
||||
throw new DockerException(request.getURI(), 500, ioe.getMessage(), null);
|
||||
throw new DockerException(host.toHostString(), request.getURI(), 500, ioe.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
private Errors getErrorsFromResponse(HttpEntity entity) {
|
||||
try {
|
||||
return SharedObjectMapper.get().readValue(entity.getContent(), Errors.class);
|
||||
}
|
||||
catch (IOException ioe) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Describes a connection to a Docker host.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public interface DockerHttpClientConnection {
|
||||
|
||||
/**
|
||||
* Create an {@link HttpHost} describing the Docker host connection.
|
||||
* @return the {@code HttpHost}
|
||||
*/
|
||||
HttpHost getHttpHost();
|
||||
|
||||
/**
|
||||
* Create an {@link HttpClient} that can be used to communicate with the Docker host.
|
||||
* @return the {@code HttpClient}
|
||||
*/
|
||||
CloseableHttpClient getHttpClient();
|
||||
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.buildpack.platform.docker;
|
||||
package org.springframework.boot.buildpack.platform.docker.httpclient;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
@ -33,8 +33,9 @@ import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket;
|
||||
* pipe.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class DockerConnectionSocketFactory implements ConnectionSocketFactory {
|
||||
class LocalDockerConnectionSocketFactory implements ConnectionSocketFactory {
|
||||
|
||||
private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock";
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.buildpack.platform.docker;
|
||||
package org.springframework.boot.buildpack.platform.docker.httpclient;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
@ -22,12 +22,13 @@ import java.net.UnknownHostException;
|
||||
import org.apache.http.conn.DnsResolver;
|
||||
|
||||
/**
|
||||
* {@link DnsResolver} used by the {@link DockerHttpClientConnectionManager} to ensure
|
||||
* only the loopback address is used.
|
||||
* {@link DnsResolver} used by the {@link LocalDockerHttpClientConnectionManager} to
|
||||
* ensure only the loopback address is used.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class DockerDnsResolver implements DnsResolver {
|
||||
class LocalDockerDnsResolver implements DnsResolver {
|
||||
|
||||
private static final InetAddress[] LOOPBACK = new InetAddress[] { InetAddress.getLoopbackAddress() };
|
||||
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.buildpack.platform.docker;
|
||||
package org.springframework.boot.buildpack.platform.docker.httpclient;
|
||||
|
||||
import org.apache.http.config.Registry;
|
||||
import org.apache.http.config.RegistryBuilder;
|
||||
@ -26,16 +26,17 @@ import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
|
||||
* {@link HttpClientConnectionManager} for Docker.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class DockerHttpClientConnectionManager extends BasicHttpClientConnectionManager {
|
||||
class LocalDockerHttpClientConnectionManager extends BasicHttpClientConnectionManager {
|
||||
|
||||
DockerHttpClientConnectionManager() {
|
||||
super(getRegistry(), null, null, new DockerDnsResolver());
|
||||
LocalDockerHttpClientConnectionManager() {
|
||||
super(getRegistry(), null, null, new LocalDockerDnsResolver());
|
||||
}
|
||||
|
||||
private static Registry<ConnectionSocketFactory> getRegistry() {
|
||||
RegistryBuilder<ConnectionSocketFactory> builder = RegistryBuilder.create();
|
||||
builder.register("docker", new DockerConnectionSocketFactory());
|
||||
builder.register("docker", new LocalDockerConnectionSocketFactory());
|
||||
return builder.build();
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.buildpack.platform.docker;
|
||||
package org.springframework.boot.buildpack.platform.docker.httpclient;
|
||||
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.conn.SchemePortResolver;
|
||||
@ -25,8 +25,9 @@ import org.apache.http.util.Args;
|
||||
* {@link SchemePortResolver} for Docker.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class DockerSchemePortResolver implements SchemePortResolver {
|
||||
class LocalDockerSchemePortResolver implements SchemePortResolver {
|
||||
|
||||
private static final int DEFAULT_DOCKER_PORT = 2376;
|
||||
|
@ -0,0 +1,167 @@
|
||||
/*
|
||||
* 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:
|
||||
*
|
||||
* <p>
|
||||
* <ul>
|
||||
* <li>{@code DOCKER_HOST} - the URL to a Docker daemon host, such as
|
||||
* {@code tcp://localhost:2376}</li>
|
||||
* <li>{@code DOCKER_TLS_VERIFY} - set to {@code 1} to enable secure connection to the
|
||||
* Docker host via TLS (optional)</li>
|
||||
* <li>{@code DOCKER_CERT_PATH} - the path to certificate and key files needed for TLS
|
||||
* verification (required if {@code DOCKER_TLS_VERIFY=1})</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 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.ssl;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Base64Utils;
|
||||
|
||||
/**
|
||||
* Parser for X.509 certificates in PEM format.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
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 CertificateParser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load certificates from the specified file paths.
|
||||
* @param certPaths one or more paths to certificate files
|
||||
* @return certificates parsed from specified file paths
|
||||
*/
|
||||
static X509Certificate[] parse(Path... certPaths) {
|
||||
List<X509Certificate> certs = new ArrayList<>();
|
||||
for (Path certFile : certPaths) {
|
||||
certs.addAll(generateCertificates(certFile));
|
||||
}
|
||||
return certs.toArray(new X509Certificate[0]);
|
||||
}
|
||||
|
||||
private static List<X509Certificate> generateCertificates(Path certPath) {
|
||||
try {
|
||||
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
|
||||
List<X509Certificate> certs = new ArrayList<>();
|
||||
|
||||
byte[] certBytes = Files.readAllBytes(certPath);
|
||||
String certString = new String(certBytes, StandardCharsets.UTF_8);
|
||||
|
||||
Matcher matcher = CERTIFICATE_PATTERN.matcher(certString);
|
||||
|
||||
while (matcher.find()) {
|
||||
byte[] content = decodeContent(matcher.group(1));
|
||||
ByteArrayInputStream contentStream = new ByteArrayInputStream(content);
|
||||
while (contentStream.available() > 0) {
|
||||
certs.add((X509Certificate) certificateFactory.generateCertificate(contentStream));
|
||||
}
|
||||
}
|
||||
|
||||
return certs;
|
||||
}
|
||||
catch (CertificateException | IOException ex) {
|
||||
throw new IllegalStateException("Error reading certificate from file " + certPath + ": " + ex.getMessage(),
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decodeContent(String content) {
|
||||
byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
|
||||
return Base64Utils.decode(contentBytes);
|
||||
}
|
||||
|
||||
}
|
@ -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.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
/**
|
||||
* Utility methods for creating Java trust material from key and certificate files.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
final class KeyStoreFactory {
|
||||
|
||||
private KeyStoreFactory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link KeyStore} populated with the certificate stored at the
|
||||
* specified file path and an optional private key.
|
||||
* @param certPath the path to the certificate authority file
|
||||
* @param keyPath the path to the private file
|
||||
* @param alias the alias to use for KeyStore entries
|
||||
* @return the {@code KeyStore}
|
||||
*/
|
||||
static KeyStore create(Path certPath, Path keyPath, String alias) {
|
||||
try {
|
||||
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
keyStore.load(null);
|
||||
|
||||
X509Certificate[] certificates = CertificateParser.parse(certPath);
|
||||
|
||||
if (keyPath != null && Files.exists(keyPath)) {
|
||||
PrivateKey privateKey = PrivateKeyParser.parse(keyPath);
|
||||
addCertsToStore(keyStore, certificates, privateKey, alias);
|
||||
}
|
||||
else {
|
||||
addCertsToStore(keyStore, certificates, alias);
|
||||
}
|
||||
|
||||
return keyStore;
|
||||
}
|
||||
catch (GeneralSecurityException | IOException ex) {
|
||||
throw new IllegalStateException("Error creating KeyStore: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
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 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]);
|
||||
}
|
||||
}
|
||||
catch (KeyStoreException ex) {
|
||||
throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Base64Utils;
|
||||
|
||||
/**
|
||||
* Parser for PKCS private key files in PEM format.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
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 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 PrivateKeyParser() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a private key from the specified file paths.
|
||||
* @param keyPath the path to the private key file
|
||||
* @return private key from specified file path
|
||||
*/
|
||||
static PrivateKey parse(Path keyPath) {
|
||||
try {
|
||||
byte[] keyBytes = Files.readAllBytes(keyPath);
|
||||
String keyString = new String(keyBytes, StandardCharsets.UTF_8);
|
||||
|
||||
Matcher matcher = PKCS_1_KEY_PATTERN.matcher(keyString);
|
||||
if (matcher.find()) {
|
||||
return parsePkcs1PrivateKey(decodeContent(matcher.group(1)));
|
||||
}
|
||||
|
||||
matcher = PKCS_8_KEY_PATTERN.matcher(keyString);
|
||||
if (matcher.find()) {
|
||||
return parsePkcs8PrivateKey(decodeContent(matcher.group(1)));
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Unrecognized private key format in " + keyPath);
|
||||
}
|
||||
catch (GeneralSecurityException | IOException ex) {
|
||||
throw new IllegalStateException("Error loading private key file " + keyPath, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] decodeContent(String content) {
|
||||
byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
|
||||
return Base64Utils.decode(contentBytes);
|
||||
}
|
||||
|
||||
private static PrivateKey parsePkcs1PrivateKey(byte[] privateKeyBytes) throws GeneralSecurityException {
|
||||
byte[] pkcs8Bytes = convertPkcs1ToPkcs8(privateKeyBytes);
|
||||
return parsePkcs8PrivateKey(pkcs8Bytes);
|
||||
}
|
||||
|
||||
private static PrivateKey parsePkcs8PrivateKey(byte[] privateKeyBytes) throws GeneralSecurityException {
|
||||
try {
|
||||
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
|
||||
return keyFactory.generatePrivate(keySpec);
|
||||
}
|
||||
catch (InvalidKeySpecException ex) {
|
||||
throw new IllegalArgumentException("Unexpected key format", ex);
|
||||
}
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.ssl;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.KeyStore;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
/**
|
||||
* Builds an {@link SSLContext} for use with an HTTP connection.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class SslContextFactory {
|
||||
|
||||
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
|
||||
* @return the {@code SSLContext}
|
||||
*/
|
||||
public SSLContext forPath(String certificatePath) {
|
||||
|
||||
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);
|
||||
|
||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
|
||||
|
||||
return sslContext;
|
||||
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -61,10 +61,11 @@ import static org.mockito.Mockito.verify;
|
||||
* Tests for {@link DockerApi}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class DockerApiTests {
|
||||
|
||||
private static final String API_URL = "docker://localhost/" + DockerApi.API_VERSION;
|
||||
private static final String API_URL = "/" + DockerApi.API_VERSION;
|
||||
|
||||
private static final String IMAGES_URL = API_URL + "/images";
|
||||
|
||||
|
@ -29,13 +29,16 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
* Tests for {@link DockerException}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class DockerExceptionTests {
|
||||
|
||||
private static final String HOST = "docker://localhost/";
|
||||
|
||||
private static final URI URI;
|
||||
static {
|
||||
try {
|
||||
URI = new URI("docker://localhost");
|
||||
URI = new URI("example");
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
@ -46,17 +49,24 @@ class DockerExceptionTests {
|
||||
|
||||
private static final Errors ERRORS = new Errors(Collections.singletonList(new Errors.Error("code", "message")));
|
||||
|
||||
@Test
|
||||
void createWhenHostIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new DockerException(null, null, 404, null, NO_ERRORS))
|
||||
.withMessage("host must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenUriIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new DockerException(null, 404, null, NO_ERRORS))
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new DockerException(this.HOST, null, 404, null, NO_ERRORS))
|
||||
.withMessage("URI must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void create() {
|
||||
DockerException exception = new DockerException(URI, 404, "missing", ERRORS);
|
||||
DockerException exception = new DockerException(HOST, URI, 404, "missing", ERRORS);
|
||||
assertThat(exception.getMessage()).isEqualTo(
|
||||
"Docker API call to 'docker://localhost' failed with status code 404 \"missing\" [code: message]");
|
||||
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]");
|
||||
assertThat(exception.getStatusCode()).isEqualTo(404);
|
||||
assertThat(exception.getReasonPhrase()).isEqualTo("missing");
|
||||
assertThat(exception.getErrors()).isSameAs(ERRORS);
|
||||
@ -64,9 +74,9 @@ class DockerExceptionTests {
|
||||
|
||||
@Test
|
||||
void createWhenReasonPhraseIsNull() {
|
||||
DockerException exception = new DockerException(URI, 404, null, ERRORS);
|
||||
assertThat(exception.getMessage())
|
||||
.isEqualTo("Docker API call to 'docker://localhost' failed with status code 404 [code: message]");
|
||||
DockerException exception = new DockerException(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);
|
||||
assertThat(exception.getReasonPhrase()).isNull();
|
||||
assertThat(exception.getErrors()).isSameAs(ERRORS);
|
||||
@ -74,15 +84,15 @@ class DockerExceptionTests {
|
||||
|
||||
@Test
|
||||
void createWhenErrorsIsNull() {
|
||||
DockerException exception = new DockerException(URI, 404, "missing", null);
|
||||
DockerException exception = new DockerException(HOST, URI, 404, "missing", null);
|
||||
assertThat(exception.getErrors()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenErrorsIsEmpty() {
|
||||
DockerException exception = new DockerException(URI, 404, "missing", NO_ERRORS);
|
||||
DockerException exception = new DockerException(HOST, URI, 404, "missing", NO_ERRORS);
|
||||
assertThat(exception.getMessage())
|
||||
.isEqualTo("Docker API call to 'docker://localhost' failed with status code 404 \"missing\"");
|
||||
.isEqualTo("Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\"");
|
||||
assertThat(exception.getStatusCode()).isEqualTo(404);
|
||||
assertThat(exception.getReasonPhrase()).isEqualTo("missing");
|
||||
assertThat(exception.getErrors()).isSameAs(NO_ERRORS);
|
||||
|
@ -25,6 +25,8 @@ import java.nio.charset.StandardCharsets;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpEntityEnclosingRequest;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.HttpRequest;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpDelete;
|
||||
@ -41,6 +43,7 @@ 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.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@ -75,6 +78,9 @@ class HttpClientHttpTests {
|
||||
@Mock
|
||||
private InputStream content;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<HttpHost> hostCaptor;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<HttpUriRequest> requestCaptor;
|
||||
|
||||
@ -85,11 +91,11 @@ class HttpClientHttpTests {
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
given(this.client.execute(any())).willReturn(this.response);
|
||||
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(this.client);
|
||||
this.uri = new URI("docker://localhost/example");
|
||||
this.http = new HttpClientHttp(new TestClientConnection(this.client));
|
||||
this.uri = new URI("example");
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -97,7 +103,7 @@ class HttpClientHttpTests {
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.get(this.uri);
|
||||
verify(this.client).execute(this.requestCaptor.capture());
|
||||
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
assertThat(request).isInstanceOf(HttpGet.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
@ -110,7 +116,7 @@ class HttpClientHttpTests {
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.post(this.uri);
|
||||
verify(this.client).execute(this.requestCaptor.capture());
|
||||
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
assertThat(request).isInstanceOf(HttpPost.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
@ -124,7 +130,7 @@ class HttpClientHttpTests {
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.post(this.uri, APPLICATION_JSON,
|
||||
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
|
||||
verify(this.client).execute(this.requestCaptor.capture());
|
||||
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
|
||||
assertThat(request).isInstanceOf(HttpPost.class);
|
||||
@ -144,7 +150,7 @@ class HttpClientHttpTests {
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.put(this.uri, APPLICATION_JSON,
|
||||
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
|
||||
verify(this.client).execute(this.requestCaptor.capture());
|
||||
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
|
||||
assertThat(request).isInstanceOf(HttpPut.class);
|
||||
@ -163,7 +169,7 @@ class HttpClientHttpTests {
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.delete(this.uri);
|
||||
verify(this.client).execute(this.requestCaptor.capture());
|
||||
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
assertThat(request).isInstanceOf(HttpDelete.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
@ -188,7 +194,8 @@ class HttpClientHttpTests {
|
||||
|
||||
@Test
|
||||
void executeWhenClientThrowsIOExceptionRethrowsAsDockerException() throws IOException {
|
||||
given(this.client.execute(any())).willThrow(new IOException("test IO exception"));
|
||||
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)
|
||||
.withMessageContaining("500")
|
||||
@ -201,4 +208,24 @@ class HttpClientHttpTests {
|
||||
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static final class TestClientConnection implements DockerHttpClientConnection {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* 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.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link CertificateParser}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class CertificateParserTests {
|
||||
|
||||
private PemFileWriter fileWriter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
this.fileWriter = new PemFileWriter();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
this.fileWriter.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseCertificates() throws IOException {
|
||||
Path caPath = this.fileWriter.writeFile("ca.pem", PemFileWriter.CA_CERTIFICATE);
|
||||
Path certPath = this.fileWriter.writeFile("cert.pem", PemFileWriter.CERTIFICATE);
|
||||
X509Certificate[] certificates = CertificateParser.parse(caPath, certPath);
|
||||
assertThat(certificates).isNotNull();
|
||||
assertThat(certificates.length).isEqualTo(2);
|
||||
assertThat(certificates[0].getType()).isEqualTo("X.509");
|
||||
assertThat(certificates[1].getType()).isEqualTo("X.509");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseCertificateChain() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("ca.pem", PemFileWriter.CA_CERTIFICATE, PemFileWriter.CERTIFICATE);
|
||||
X509Certificate[] certificates = CertificateParser.parse(path);
|
||||
assertThat(certificates).isNotNull();
|
||||
assertThat(certificates.length).isEqualTo(2);
|
||||
assertThat(certificates[0].getType()).isEqualTo("X.509");
|
||||
assertThat(certificates[1].getType()).isEqualTo("X.509");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithInvalidPathWillThrowException() throws URISyntaxException {
|
||||
Path path = Paths.get(new URI("file:///bad/path/cert.pem"));
|
||||
assertThatIllegalStateException().isThrownBy(() -> CertificateParser.parse(path))
|
||||
.withMessageContaining(path.toString());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link KeyStoreFactory}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class KeyStoreFactoryTests {
|
||||
|
||||
private PemFileWriter fileWriter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
this.fileWriter = new PemFileWriter();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
this.fileWriter.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createKeyStoreWithCertChain()
|
||||
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);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createKeyStoreWithCertChainAndPrivateKey()
|
||||
throws IOException, KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.springframework.util.FileSystemUtils;
|
||||
|
||||
/**
|
||||
* Utility to write certificate and key PEM files for testing.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
public class PemFileWriter {
|
||||
|
||||
private static final String EXAMPLE_SECRET_QUALIFIER = "example";
|
||||
|
||||
public static final String CA_CERTIFICATE = "-----BEGIN TRUSTED CERTIFICATE-----\n"
|
||||
+ "MIIClzCCAgACCQCPbjkRoMVEQDANBgkqhkiG9w0BAQUFADCBjzELMAkGA1UEBhMC\n"
|
||||
+ "VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28x\n"
|
||||
+ "DTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxFDASBgNVBAMMC2V4YW1wbGUu\n"
|
||||
+ "Y29tMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMB4XDTIwMDMyNzIx\n"
|
||||
+ "NTgwNFoXDTIxMDMyNzIxNTgwNFowgY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD\n"
|
||||
+ "YWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKDARUZXN0\n"
|
||||
+ "MQ0wCwYDVQQLDARUZXN0MRQwEgYDVQQDDAtleGFtcGxlLmNvbTEfMB0GCSqGSIb3\n"
|
||||
+ "DQEJARYQdGVzdEBleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC\n"
|
||||
+ "gYEA1YzixWEoyzrd20C2R1gjyPCoPfFLlG6UYTyT0tueNy6yjv6qbJ8lcZg7616O\n"
|
||||
+ "3I9LuOHhZh9U+fCDCgPfiDdyJfDEW/P+dsOMFyMUXPrJPze2yPpOnvV8iJ5DM93u\n"
|
||||
+ "fEVhCCyzLdYu0P2P3hU2W+T3/Im9DA7FOPA2vF1SrIJ2qtUCAwEAATANBgkqhkiG\n"
|
||||
+ "9w0BAQUFAAOBgQBdShkwUv78vkn1jAdtfbB+7mpV9tufVdo29j7pmotTCz3ny5fc\n"
|
||||
+ "zLEfeu6JPugAR71JYbc2CqGrMneSk1zT91EH6ohIz8OR5VNvzB7N7q65Ci7OFMPl\n"
|
||||
+ "ly6k3rHpMCBtHoyNFhNVfPLxGJ9VlWFKLgIAbCmL4OIQm1l6Fr1MSM38Zw==\n" + "-----END TRUSTED CERTIFICATE-----\n";
|
||||
|
||||
public static final String CA_PRIVATE_KEY = EXAMPLE_SECRET_QUALIFIER + "-----BEGIN PRIVATE KEY-----\n"
|
||||
+ "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANWM4sVhKMs63dtA\n"
|
||||
+ "tkdYI8jwqD3xS5RulGE8k9Lbnjcuso7+qmyfJXGYO+tejtyPS7jh4WYfVPnwgwoD\n"
|
||||
+ "34g3ciXwxFvz/nbDjBcjFFz6yT83tsj6Tp71fIieQzPd7nxFYQgssy3WLtD9j94V\n"
|
||||
+ "Nlvk9/yJvQwOxTjwNrxdUqyCdqrVAgMBAAECgYEAyJTlZ8nj3Eg1nLxCue6C5jmN\n"
|
||||
+ "fWkIuanH+zFAE/0utdxJ4WA4yYAOVo1MMr8FZwu9bzHTWe2yDnWnT5/ltPeHYX2X\n"
|
||||
+ "9Pg5cY0tjq07utaMwLKWgJ0Xoh2UpVM799t/rSvMWmLaZ2c8nipX+gQfYJFpX8Vg\n"
|
||||
+ "mR3QPxwdmNyFo13qif0CQQD4z2SqCfARuxscTCJDZ6wReikMQxaJvq74lPEtT26L\n"
|
||||
+ "rBr/bN+mG7+rMEHxs5wtU47aNjUKuVVC0Qfhsf95ahvHAkEA27inSlxrwGvhvFsD\n"
|
||||
+ "FWdgDsfYpPZdL4YgpVSEvcoypRGg2suJw2omcKcY56XpkmWUqZc06QirumtnEC0P\n"
|
||||
+ "HfnsgwJBAMVhEURrOc13FxytsQiz96atuF6H4htH79o3ndQKDXI0B/7VSd6maLjP\n"
|
||||
+ "QaESkTTL8qldE1r8h4zH8m6zHC4fZQUCQFWJ+8bdWC2fUlBr9jVc+26Fqvf92aVo\n"
|
||||
+ "yEjVMKBamYDd7gt/9fAX4UM2KmH0m4wc89VaQoT+lSyMJ6GKiToYVFUCQEXcyoeO\n"
|
||||
+ "zWqtSgEX/eXQXzmMKxYnjv1O//ba3Q7UiHd/XO5j4QXAJpcB6h0h00uC5KY2d0Zy\n" + "JQ1kB1C2l6l9tyc=\n"
|
||||
+ "-----END PRIVATE KEY-----";
|
||||
|
||||
public static final String CERTIFICATE = "-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIICjzCCAfgCAQEwDQYJKoZIhvcNAQEFBQAwgY8xCzAJBgNVBAYTAlVTMRMwEQYD\n"
|
||||
+ "VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQK\n"
|
||||
+ "DARUZXN0MQ0wCwYDVQQLDARUZXN0MRQwEgYDVQQDDAtleGFtcGxlLmNvbTEfMB0G\n"
|
||||
+ "CSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTAeFw0yMDAzMjcyMjAxNDZaFw0y\n"
|
||||
+ "MTAzMjcyMjAxNDZaMIGPMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5p\n"
|
||||
+ "YTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwEVGVzdDENMAsGA1UE\n"
|
||||
+ "CwwEVGVzdDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xHzAdBgkqhkiG9w0BCQEWEHRl\n"
|
||||
+ "c3RAZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM7kd2cj\n"
|
||||
+ "F49wm1+OQ7Q5GE96cXueWNPr/Nwei71tf6G4BmE0B+suXHEvnLpHTj9pdX/ZzBIK\n"
|
||||
+ "8jIZ/x8RnSduK/Ky+zm1QMYUWZtWCAgCW8WzgB69Cn/hQG8KSX3S9bqODuQAvP54\n"
|
||||
+ "GQJD7+4kVuNBGjFb4DaD4nvMmPtALSZf8ZCZAgMBAAEwDQYJKoZIhvcNAQEFBQAD\n"
|
||||
+ "gYEAOn6X8+0VVlDjF+TvTgI0KIasA6nDm+KXe7LVtfvqWqQZH4qyd2uiwcDM3Aux\n"
|
||||
+ "a/OsPdOw0j+NqFDBd3mSMhSVgfvXdK6j9WaxY1VGXyaidLARgvn63wfzgr857sQW\n"
|
||||
+ "c8eSxbwEQxwlMvVxW6Os4VhCfUQr8VrBrvPa2zs+6IlK+Ug=\n" + "-----END CERTIFICATE-----\n";
|
||||
|
||||
public static final String PRIVATE_KEY = EXAMPLE_SECRET_QUALIFIER + "-----BEGIN RSA PRIVATE KEY-----\n"
|
||||
+ "MIICXAIBAAKBgQDO5HdnIxePcJtfjkO0ORhPenF7nljT6/zcHou9bX+huAZhNAfr\n"
|
||||
+ "LlxxL5y6R04/aXV/2cwSCvIyGf8fEZ0nbivysvs5tUDGFFmbVggIAlvFs4AevQp/\n"
|
||||
+ "4UBvCkl90vW6jg7kALz+eBkCQ+/uJFbjQRoxW+A2g+J7zJj7QC0mX/GQmQIDAQAB\n"
|
||||
+ "AoGAIWPsBWA7gDHrUYuzT5XbX5BiWlIfAezXPWtMoEDY1W/Oz8dG8+TilH3brJCv\n"
|
||||
+ "hzps9TpgXhUYK4/Yhdog4+k6/EEY80RvcObOnflazTCVS041B0Ipm27uZjIq2+1F\n"
|
||||
+ "ZfbWP+B3crpzh8wvIYA+6BCcZV9zi8Od32NEs39CtrOrFPUCQQDxnt9+JlWjtteR\n"
|
||||
+ "VttRSKjtzKIF08BzNuZlRP9HNWveLhphIvdwBfjASwqgtuslqziEnGG8kniWzyYB\n"
|
||||
+ "a/ZZVoT3AkEA2zSBMpvGPDkGbOMqbnR8UL3uijkOj+blQe1gsyu3dUa9T42O1u9h\n"
|
||||
+ "Iz5SdCYlSFHbDNRFrwuW2QnhippqIQqC7wJAbVeyWEpM0yu5XiJqWdyB5iuG3xA2\n"
|
||||
+ "tW0Q0p9ozvbT+9XtRiwmweFR8uOCybw9qexURV7ntAis3cKctmP/Neq7fQJBAKGa\n"
|
||||
+ "59UjutYTRIVqRJICFtR/8ii9P9sfYs1j7/KnvC0d5duMhU44VOjivW8b4Eic8F1Y\n"
|
||||
+ "8bbHWILSIhFJHg0V7skCQDa8/YkRWF/3pwIZNWQr4ce4OzvYsFMkRvGRdX8B2a0p\n"
|
||||
+ "wSKcVTdEdO2DhBlYddN0zG0rjq4vDMtdmldEl4BdldQ=\n" + "-----END RSA PRIVATE KEY-----\n";
|
||||
|
||||
private final Path tempDir;
|
||||
|
||||
public PemFileWriter() throws IOException {
|
||||
this.tempDir = Files.createTempDirectory("buildpack-platform-docker-ssl-tests");
|
||||
}
|
||||
|
||||
Path writeFile(String name, String... contents) throws IOException {
|
||||
Path path = Paths.get(this.tempDir.toString(), name);
|
||||
for (String content : contents) {
|
||||
Files.write(path, content.replaceAll(EXAMPLE_SECRET_QUALIFIER, "").getBytes(), StandardOpenOption.CREATE,
|
||||
StandardOpenOption.APPEND);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public Path getTempDir() {
|
||||
return this.tempDir;
|
||||
}
|
||||
|
||||
void cleanup() throws IOException {
|
||||
FileSystemUtils.deleteRecursively(this.tempDir);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.PrivateKey;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link PrivateKeyParser}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class PrivateKeyParserTests {
|
||||
|
||||
private PemFileWriter fileWriter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
this.fileWriter = new PemFileWriter();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
this.fileWriter.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePkcs8KeyFile() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.CA_PRIVATE_KEY);
|
||||
PrivateKey privateKey = PrivateKeyParser.parse(path);
|
||||
assertThat(privateKey).isNotNull();
|
||||
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
|
||||
Files.delete(path);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsePkcs1KeyFile() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_KEY);
|
||||
PrivateKey privateKey = PrivateKeyParser.parse(path);
|
||||
assertThat(privateKey).isNotNull();
|
||||
// keys in PKCS#1 format are converted to PKCS#8 for parsing
|
||||
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
|
||||
Files.delete(path);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithNonKeyFileWillThrowException() throws IOException {
|
||||
Path path = this.fileWriter.writeFile("text.pem", "plain text");
|
||||
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(path))
|
||||
.withMessageContaining(path.toString());
|
||||
Files.delete(path);
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWithInvalidPathWillThrowException() throws URISyntaxException {
|
||||
URI privateKeyPath = new URI("file:///bad/path/key.pem");
|
||||
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(Paths.get(privateKeyPath)))
|
||||
.withMessageContaining(privateKeyPath.getPath());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.ssl;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link SslContextFactory}.
|
||||
*
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class SslContextFactoryTests {
|
||||
|
||||
private PemFileWriter fileWriter;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
this.fileWriter = new PemFileWriter();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() throws IOException {
|
||||
this.fileWriter.cleanup();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createKeyStoreWithCertChain() throws IOException {
|
||||
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());
|
||||
assertThat(sslContext).isNotNull();
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user