Restructure and polish docker code
Restructure and polish docker code to fix a package tangle and provide better separation of concerns.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ConnectionSocketFactory> getRegistry() {
|
||||
RegistryBuilder<ConnectionSocketFactory> builder = RegistryBuilder.create();
|
||||
builder.register("docker", new LocalDockerConnectionSocketFactory());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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:
|
||||
*
|
||||
* <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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<X509Certificate> certs = new ArrayList<>();
|
||||
for (Path certFile : certPaths) {
|
||||
certs.addAll(generateCertificates(certFile));
|
||||
static X509Certificate[] parse(Path... paths) {
|
||||
CertificateFactory factory = getCertificateFactory();
|
||||
List<X509Certificate> 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<X509Certificate> generateCertificates(Path certPath) {
|
||||
private static CertificateFactory getCertificateFactory() {
|
||||
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);
|
||||
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<X509Certificate> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 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 PrivateKey getPrivateKey(Path path) {
|
||||
if (path != null && Files.exists(path)) {
|
||||
return PrivateKeyParser.parse(path);
|
||||
}
|
||||
catch (KeyStoreException ex) {
|
||||
throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey,
|
||||
String alias) throws KeyStoreException {
|
||||
if (privateKey != null) {
|
||||
keyStore.setKeyEntry(alias, privateKey, NO_PASSWORD, certificates);
|
||||
}
|
||||
else {
|
||||
for (int index = 0; index < certificates.length; index++) {
|
||||
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 parsePkcs1PrivateKey(byte[] privateKeyBytes) throws GeneralSecurityException {
|
||||
private static PrivateKey parsePkcs1(byte[] privateKeyBytes) throws GeneralSecurityException {
|
||||
byte[] pkcs8Bytes = convertPkcs1ToPkcs8(privateKeyBytes);
|
||||
return parsePkcs8PrivateKey(pkcs8Bytes);
|
||||
return parsePkcs8(pkcs8Bytes);
|
||||
}
|
||||
|
||||
private static PrivateKey parsePkcs8PrivateKey(byte[] privateKeyBytes) throws GeneralSecurityException {
|
||||
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 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 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 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... 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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;
|
||||
@@ -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<ConnectionSocketFactory> getRegistry() {
|
||||
RegistryBuilder<ConnectionSocketFactory> 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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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<HttpUriRequest> 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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, String> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<String, String> 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<HttpHost> 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);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user