Restructure and polish docker code

Restructure and polish docker code to fix a package tangle and
provide better separation of concerns.
pull/20756/head
Phillip Webb 5 years ago
parent 5d8d0bb159
commit 4dad56a491

@ -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 PrivateKey getPrivateKey(Path path) {
if (path != null && Files.exists(path)) {
return PrivateKeyParser.parse(path);
}
return null;
}
private static void addCertsToStore(KeyStore keyStore, X509Certificate[] certs, String alias) {
try {
for (int index = 0; index < certs.length; index++) {
String indexedAlias = alias + "-" + index;
keyStore.setCertificateEntry(indexedAlias, certs[index]);
}
private static void addCertificates(KeyStore keyStore, X509Certificate[] certificates, PrivateKey privateKey,
String alias) throws KeyStoreException {
if (privateKey != null) {
keyStore.setKeyEntry(alias, privateKey, NO_PASSWORD, certificates);
}
catch (KeyStoreException ex) {
throw new IllegalStateException("Error adding certificates to KeyStore: " + ex.getMessage(), ex);
else {
for (int index = 0; index < certificates.length; index++) {
keyStore.setCertificateEntry(alias + "-" + index, certificates[index]);
}
}
}

@ -16,6 +16,7 @@
package org.springframework.boot.buildpack.platform.docker.ssl;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@ -34,62 +35,93 @@ import org.springframework.util.Base64Utils;
* Parser for PKCS private key files in PEM format.
*
* @author Scott Frederick
* @author Phillip Webb
*/
final class PrivateKeyParser {
private static final Pattern PKCS_1_KEY_PATTERN = Pattern
.compile("-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header
"([a-z0-9+/=\\r\\n]+)" + // Base64 text
"-+END\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+", // Footer
Pattern.CASE_INSENSITIVE);
private static final String PKCS1_HEADER = "-+BEGIN\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
private static final Pattern PKCS_8_KEY_PATTERN = Pattern
.compile("-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header
"([a-z0-9+/=\\r\\n]+)" + // Base64 text
"-+END\\s+PRIVATE\\s+KEY[^-]*-+", // Footer
Pattern.CASE_INSENSITIVE);
private static final String PKCS1_FOOTER = "-+END\\s+RSA\\s+PRIVATE\\s+KEY[^-]*-+";
private static final String PKCS8_FOOTER = "-+END\\s+PRIVATE\\s+KEY[^-]*-+";
private static final String PKCS8_HEADER = "-+BEGIN\\s+PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+";
private static final String BASE64_TEXT = "([a-z0-9+/=\\r\\n]+)";
private static final Pattern PKCS1_PATTERN = Pattern.compile(PKCS1_HEADER + BASE64_TEXT + PKCS1_FOOTER,
Pattern.CASE_INSENSITIVE);
private static final Pattern PKCS8_KEY_PATTERN = Pattern.compile(PKCS8_HEADER + BASE64_TEXT + PKCS8_FOOTER,
Pattern.CASE_INSENSITIVE);
private PrivateKeyParser() {
}
/**
* Load a private key from the specified file paths.
* @param keyPath the path to the private key file
* @param path the path to the private key file
* @return private key from specified file path
*/
static PrivateKey parse(Path keyPath) {
static PrivateKey parse(Path path) {
try {
byte[] keyBytes = Files.readAllBytes(keyPath);
String keyString = new String(keyBytes, StandardCharsets.UTF_8);
Matcher matcher = PKCS_1_KEY_PATTERN.matcher(keyString);
String text = readText(path);
Matcher matcher = PKCS1_PATTERN.matcher(text);
if (matcher.find()) {
return parsePkcs1PrivateKey(decodeContent(matcher.group(1)));
return parsePkcs1(decodeBase64(matcher.group(1)));
}
matcher = PKCS_8_KEY_PATTERN.matcher(keyString);
matcher = PKCS8_KEY_PATTERN.matcher(text);
if (matcher.find()) {
return parsePkcs8PrivateKey(decodeContent(matcher.group(1)));
return parsePkcs8(decodeBase64(matcher.group(1)));
}
throw new IllegalStateException("Unrecognized private key format in " + keyPath);
throw new IllegalStateException("Unrecognized private key format in " + path);
}
catch (GeneralSecurityException | IOException ex) {
throw new IllegalStateException("Error loading private key file " + keyPath, ex);
throw new IllegalStateException("Error loading private key file " + path, ex);
}
}
private static byte[] decodeContent(String content) {
byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
return Base64Utils.decode(contentBytes);
private static PrivateKey parsePkcs1(byte[] privateKeyBytes) throws GeneralSecurityException {
byte[] pkcs8Bytes = convertPkcs1ToPkcs8(privateKeyBytes);
return parsePkcs8(pkcs8Bytes);
}
private static PrivateKey parsePkcs1PrivateKey(byte[] privateKeyBytes) throws GeneralSecurityException {
byte[] pkcs8Bytes = convertPkcs1ToPkcs8(privateKeyBytes);
return parsePkcs8PrivateKey(pkcs8Bytes);
private static byte[] convertPkcs1ToPkcs8(byte[] pkcs1) {
try {
ByteArrayOutputStream result = new ByteArrayOutputStream();
int pkcs1Length = pkcs1.length;
int totalLength = pkcs1Length + 22;
// Sequence + total length
result.write(bytes(0x30, 0x82));
result.write((totalLength >> 8) & 0xff);
result.write(totalLength & 0xff);
// Integer (0)
result.write(bytes(0x02, 0x01, 0x00));
// Sequence: 1.2.840.113549.1.1.1, NULL
result.write(
bytes(0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00));
// Octet string + length
result.write(bytes(0x04, 0x82));
result.write((pkcs1Length >> 8) & 0xff);
result.write(pkcs1Length & 0xff);
// PKCS1
result.write(pkcs1);
return result.toByteArray();
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private static PrivateKey parsePkcs8PrivateKey(byte[] privateKeyBytes) throws GeneralSecurityException {
private static byte[] bytes(int... elements) {
byte[] result = new byte[elements.length];
for (int i = 0; i < elements.length; i++) {
result[i] = (byte) elements[i];
}
return result;
}
private static PrivateKey parsePkcs8(byte[] privateKeyBytes) throws GeneralSecurityException {
try {
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
@ -100,26 +132,14 @@ final class PrivateKeyParser {
}
}
private static byte[] convertPkcs1ToPkcs8(byte[] privateKeyBytes) {
int pkcs1Length = privateKeyBytes.length;
int totalLength = pkcs1Length + 22;
byte[] pkcs8Header = new byte[] { 0x30, (byte) 0x82, (byte) ((totalLength >> 8) & 0xff),
// Sequence + total length
(byte) (totalLength & 0xff),
// Integer (0)
0x2, 0x1, 0x0,
// Sequence: 1.2.840.113549.1.1.1, NULL
0x30, 0xD, 0x6, 0x9, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0,
// Octet string + length
0x4, (byte) 0x82, (byte) ((pkcs1Length >> 8) & 0xff), (byte) (pkcs1Length & 0xff) };
return join(pkcs8Header, privateKeyBytes);
private static String readText(Path path) throws IOException {
byte[] bytes = Files.readAllBytes(path);
return new String(bytes, StandardCharsets.UTF_8);
}
private static byte[] join(byte[] byteArray1, byte[] byteArray2) {
byte[] bytes = new byte[byteArray1.length + byteArray2.length];
System.arraycopy(byteArray1, 0, bytes, 0, byteArray1.length);
System.arraycopy(byteArray2, 0, bytes, byteArray1.length, byteArray2.length);
return bytes;
private static byte[] decodeBase64(String content) {
byte[] contentBytes = content.replaceAll("\r", "").replaceAll("\n", "").getBytes();
return Base64Utils.decode(contentBytes);
}
}

@ -20,71 +20,77 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import org.springframework.util.Assert;
/**
* Builds an {@link SSLContext} for use with an HTTP connection.
*
* @author Scott Frederick
* @author Phillip Webb
* @since 2.3.0
*/
public class SslContextFactory {
private static final char[] NO_PASSWORD = {};
private static final String KEY_STORE_ALIAS = "spring-boot-docker";
public SslContextFactory() {
}
/**
* Create an {@link SSLContext} from files in the specified directory.
*
* The directory must contain files with the names 'key.pem', 'cert.pem', and
* 'ca.pem'.
* @param certificatePath the path to a directory containing certificate and key files
* Create an {@link SSLContext} from files in the specified directory. The directory
* must contain files with the names 'key.pem', 'cert.pem', and 'ca.pem'.
* @param directory the path to a directory containing certificate and key files
* @return the {@code SSLContext}
*/
public SSLContext forPath(String certificatePath) {
public SSLContext forDirectory(String directory) {
try {
Path keyPath = Paths.get(certificatePath, "key.pem");
Path certPath = Paths.get(certificatePath, "cert.pem");
Path certAuthorityPath = Paths.get(certificatePath, "ca.pem");
Path certAuthorityKeyPath = Paths.get(certificatePath, "ca-key.pem");
verifyCertificateFiles(keyPath, certPath, certAuthorityPath);
KeyStore keyStore = KeyStoreFactory.create(certPath, keyPath, KEY_STORE_ALIAS);
KeyManagerFactory keyManagerFactory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, new char[] {});
KeyStore trustStore = KeyStoreFactory.create(certAuthorityPath, certAuthorityKeyPath, KEY_STORE_ALIAS);
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
Path keyPath = Paths.get(directory, "key.pem");
Path certPath = Paths.get(directory, "cert.pem");
Path caPath = Paths.get(directory, "ca.pem");
Path caKeyPath = Paths.get(directory, "ca-key.pem");
verifyCertificateFiles(keyPath, certPath, caPath);
KeyManagerFactory keyManagerFactory = getKeyManagerFactory(keyPath, certPath);
TrustManagerFactory trustManagerFactory = getTrustManagerFactory(caPath, caKeyPath);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
return sslContext;
}
catch (RuntimeException ex) {
throw ex;
}
catch (Exception ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
private KeyManagerFactory getKeyManagerFactory(Path keyPath, Path certPath) throws Exception {
KeyStore store = KeyStoreFactory.create(certPath, keyPath, KEY_STORE_ALIAS);
KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
factory.init(store, NO_PASSWORD);
return factory;
}
private TrustManagerFactory getTrustManagerFactory(Path caPath, Path caKeyPath)
throws NoSuchAlgorithmException, KeyStoreException {
KeyStore store = KeyStoreFactory.create(caPath, caKeyPath, KEY_STORE_ALIAS);
TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
factory.init(store);
return factory;
}
private static void verifyCertificateFiles(Path... certificateFilePaths) {
for (Path path : certificateFilePaths) {
if (!Files.exists(path)) {
throw new RuntimeException(
"Certificate path must contain the files 'ca.pem', 'cert.pem', and 'key.pem'");
}
private static void verifyCertificateFiles(Path... paths) {
for (Path path : paths) {
Assert.state(Files.exists(path) && Files.isRegularFile(path),
"Certificate path must contain the files 'ca.pem', 'cert.pem', and 'key.pem' files");
}
}

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

Loading…
Cancel
Save