Support remote Docker daemon for building images

Prior to this commit, the build plugin goal/task for building images
required a locally running Docker daemon that was accessed via a
non-networked socket or pipe.

This commit adds support for remote Docker daemons at a location
specified by the environment variable `DOCKER_HOST`. Additional
environment variables `DOCKER_TLS_VERIFY` and `DOCKER_CERT_PATH`
are recognized for configuring a secure TLS connection to the daemon.

Fixes gh-20538
This commit is contained in:
Scott Frederick
2020-03-24 18:42:07 -05:00
parent fd05bc2a4a
commit ed6e54218d
24 changed files with 1400 additions and 55 deletions

View File

@@ -61,10 +61,11 @@ import static org.mockito.Mockito.verify;
* Tests for {@link DockerApi}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class DockerApiTests {
private static final String API_URL = "docker://localhost/" + DockerApi.API_VERSION;
private static final String API_URL = "/" + DockerApi.API_VERSION;
private static final String IMAGES_URL = API_URL + "/images";

View File

@@ -29,13 +29,16 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* Tests for {@link DockerException}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class DockerExceptionTests {
private static final String HOST = "docker://localhost/";
private static final URI URI;
static {
try {
URI = new URI("docker://localhost");
URI = new URI("example");
}
catch (URISyntaxException ex) {
throw new IllegalStateException(ex);
@@ -46,17 +49,24 @@ class DockerExceptionTests {
private static final Errors ERRORS = new Errors(Collections.singletonList(new Errors.Error("code", "message")));
@Test
void createWhenHostIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new DockerException(null, null, 404, null, NO_ERRORS))
.withMessage("host must not be null");
}
@Test
void createWhenUriIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new DockerException(null, 404, null, NO_ERRORS))
assertThatIllegalArgumentException()
.isThrownBy(() -> new DockerException(this.HOST, null, 404, null, NO_ERRORS))
.withMessage("URI must not be null");
}
@Test
void create() {
DockerException exception = new DockerException(URI, 404, "missing", ERRORS);
DockerException exception = new DockerException(HOST, URI, 404, "missing", ERRORS);
assertThat(exception.getMessage()).isEqualTo(
"Docker API call to 'docker://localhost' failed with status code 404 \"missing\" [code: message]");
"Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\" [code: message]");
assertThat(exception.getStatusCode()).isEqualTo(404);
assertThat(exception.getReasonPhrase()).isEqualTo("missing");
assertThat(exception.getErrors()).isSameAs(ERRORS);
@@ -64,9 +74,9 @@ class DockerExceptionTests {
@Test
void createWhenReasonPhraseIsNull() {
DockerException exception = new DockerException(URI, 404, null, ERRORS);
assertThat(exception.getMessage())
.isEqualTo("Docker API call to 'docker://localhost' failed with status code 404 [code: message]");
DockerException exception = new DockerException(HOST, URI, 404, null, ERRORS);
assertThat(exception.getMessage()).isEqualTo(
"Docker API call to 'docker://localhost/example' failed with status code 404 [code: message]");
assertThat(exception.getStatusCode()).isEqualTo(404);
assertThat(exception.getReasonPhrase()).isNull();
assertThat(exception.getErrors()).isSameAs(ERRORS);
@@ -74,15 +84,15 @@ class DockerExceptionTests {
@Test
void createWhenErrorsIsNull() {
DockerException exception = new DockerException(URI, 404, "missing", null);
DockerException exception = new DockerException(HOST, URI, 404, "missing", null);
assertThat(exception.getErrors()).isNull();
}
@Test
void createWhenErrorsIsEmpty() {
DockerException exception = new DockerException(URI, 404, "missing", NO_ERRORS);
DockerException exception = new DockerException(HOST, URI, 404, "missing", NO_ERRORS);
assertThat(exception.getMessage())
.isEqualTo("Docker API call to 'docker://localhost' failed with status code 404 \"missing\"");
.isEqualTo("Docker API call to 'docker://localhost/example' failed with status code 404 \"missing\"");
assertThat(exception.getStatusCode()).isEqualTo(404);
assertThat(exception.getReasonPhrase()).isEqualTo("missing");
assertThat(exception.getErrors()).isSameAs(NO_ERRORS);

View File

@@ -25,6 +25,8 @@ import java.nio.charset.StandardCharsets;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpDelete;
@@ -41,6 +43,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.buildpack.platform.docker.Http.Response;
import org.springframework.boot.buildpack.platform.docker.httpclient.DockerHttpClientConnection;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -75,6 +78,9 @@ class HttpClientHttpTests {
@Mock
private InputStream content;
@Captor
private ArgumentCaptor<HttpHost> hostCaptor;
@Captor
private ArgumentCaptor<HttpUriRequest> requestCaptor;
@@ -85,11 +91,11 @@ class HttpClientHttpTests {
@BeforeEach
void setup() throws Exception {
MockitoAnnotations.initMocks(this);
given(this.client.execute(any())).willReturn(this.response);
given(this.client.execute(any(HttpHost.class), any(HttpRequest.class))).willReturn(this.response);
given(this.response.getEntity()).willReturn(this.entity);
given(this.response.getStatusLine()).willReturn(this.statusLine);
this.http = new HttpClientHttp(this.client);
this.uri = new URI("docker://localhost/example");
this.http = new HttpClientHttp(new TestClientConnection(this.client));
this.uri = new URI("example");
}
@Test
@@ -97,7 +103,7 @@ class HttpClientHttpTests {
given(this.entity.getContent()).willReturn(this.content);
given(this.statusLine.getStatusCode()).willReturn(200);
Response response = this.http.get(this.uri);
verify(this.client).execute(this.requestCaptor.capture());
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
HttpUriRequest request = this.requestCaptor.getValue();
assertThat(request).isInstanceOf(HttpGet.class);
assertThat(request.getURI()).isEqualTo(this.uri);
@@ -110,7 +116,7 @@ class HttpClientHttpTests {
given(this.entity.getContent()).willReturn(this.content);
given(this.statusLine.getStatusCode()).willReturn(200);
Response response = this.http.post(this.uri);
verify(this.client).execute(this.requestCaptor.capture());
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
HttpUriRequest request = this.requestCaptor.getValue();
assertThat(request).isInstanceOf(HttpPost.class);
assertThat(request.getURI()).isEqualTo(this.uri);
@@ -124,7 +130,7 @@ class HttpClientHttpTests {
given(this.statusLine.getStatusCode()).willReturn(200);
Response response = this.http.post(this.uri, APPLICATION_JSON,
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
verify(this.client).execute(this.requestCaptor.capture());
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
HttpUriRequest request = this.requestCaptor.getValue();
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
assertThat(request).isInstanceOf(HttpPost.class);
@@ -144,7 +150,7 @@ class HttpClientHttpTests {
given(this.statusLine.getStatusCode()).willReturn(200);
Response response = this.http.put(this.uri, APPLICATION_JSON,
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
verify(this.client).execute(this.requestCaptor.capture());
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
HttpUriRequest request = this.requestCaptor.getValue();
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
assertThat(request).isInstanceOf(HttpPut.class);
@@ -163,7 +169,7 @@ class HttpClientHttpTests {
given(this.entity.getContent()).willReturn(this.content);
given(this.statusLine.getStatusCode()).willReturn(200);
Response response = this.http.delete(this.uri);
verify(this.client).execute(this.requestCaptor.capture());
verify(this.client).execute(this.hostCaptor.capture(), this.requestCaptor.capture());
HttpUriRequest request = this.requestCaptor.getValue();
assertThat(request).isInstanceOf(HttpDelete.class);
assertThat(request.getURI()).isEqualTo(this.uri);
@@ -188,7 +194,8 @@ class HttpClientHttpTests {
@Test
void executeWhenClientThrowsIOExceptionRethrowsAsDockerException() throws IOException {
given(this.client.execute(any())).willThrow(new IOException("test IO exception"));
given(this.client.execute(any(HttpHost.class), any(HttpRequest.class)))
.willThrow(new IOException("test IO exception"));
assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri))
.satisfies((ex) -> assertThat(ex.getErrors()).isNull()).satisfies(DockerException::getStatusCode)
.withMessageContaining("500")
@@ -201,4 +208,24 @@ class HttpClientHttpTests {
return new String(out.toByteArray(), StandardCharsets.UTF_8);
}
private static final class TestClientConnection implements DockerHttpClientConnection {
private final CloseableHttpClient client;
private TestClientConnection(CloseableHttpClient client) {
this.client = client;
}
@Override
public HttpHost getHttpHost() {
return HttpHost.create("docker://localhost");
}
@Override
public CloseableHttpClient getHttpClient() {
return this.client;
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.docker.httpclient;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpHost;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.httpclient.RemoteEnvironmentDockerHttpClientConnection.EnvironmentAccessor;
import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RemoteEnvironmentDockerHttpClientConnection}.
*
* @author Scott Frederick
*/
class RemoteEnvironmentDockerHttpClientConnectionTests {
private EnvironmentAccessor environment;
private RemoteEnvironmentDockerHttpClientConnection connection;
private SslContextFactory sslContextFactory;
@BeforeEach
void setUp() {
this.environment = mock(EnvironmentAccessor.class);
this.sslContextFactory = mock(SslContextFactory.class);
this.connection = new RemoteEnvironmentDockerHttpClientConnection(this.environment, this.sslContextFactory);
}
@Test
void notAcceptedWhenDockerHostNotSet() {
assertThat(this.connection.accept()).isFalse();
assertThatIllegalStateException().isThrownBy(() -> this.connection.getHttpHost());
assertThatIllegalStateException().isThrownBy(() -> this.connection.getHttpClient());
}
@Test
void acceptedWhenDockerHostIsSet() {
given(this.environment.getProperty("DOCKER_HOST")).willReturn("tcp://192.168.1.2:2376");
assertThat(this.connection.accept()).isTrue();
}
@Test
void invalidTlsConfigurationThrowsException() {
given(this.environment.getProperty("DOCKER_HOST")).willReturn("tcp://192.168.1.2:2376");
given(this.environment.getProperty("DOCKER_TLS_VERIFY")).willReturn("1");
assertThatIllegalArgumentException().isThrownBy(() -> this.connection.accept())
.withMessageContaining("DOCKER_CERT_PATH");
}
@Test
void hostProtocolIsHttpWhenNotSecure() {
given(this.environment.getProperty("DOCKER_HOST")).willReturn("tcp://192.168.1.2:2376");
assertThat(this.connection.accept()).isTrue();
HttpHost host = this.connection.getHttpHost();
assertThat(host).isNotNull();
assertThat(host.getSchemeName()).isEqualTo("http");
assertThat(host.getHostName()).isEqualTo("192.168.1.2");
assertThat(host.getPort()).isEqualTo(2376);
}
@Test
void hostProtocolIsHttpsWhenSecure() throws NoSuchAlgorithmException {
given(this.environment.getProperty("DOCKER_HOST")).willReturn("tcp://192.168.1.2:2376");
given(this.environment.getProperty("DOCKER_TLS_VERIFY")).willReturn("1");
given(this.environment.getProperty("DOCKER_CERT_PATH")).willReturn("/test-cert-path");
given(this.sslContextFactory.forPath("/test-cert-path")).willReturn(SSLContext.getDefault());
assertThat(this.connection.accept()).isTrue();
HttpHost host = this.connection.getHttpHost();
assertThat(host).isNotNull();
assertThat(host.getSchemeName()).isEqualTo("https");
assertThat(host.getHostName()).isEqualTo("192.168.1.2");
assertThat(host.getPort()).isEqualTo(2376);
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.docker.ssl;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.cert.X509Certificate;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link CertificateParser}.
*
* @author Scott Frederick
*/
class CertificateParserTests {
private PemFileWriter fileWriter;
@BeforeEach
void setUp() throws IOException {
this.fileWriter = new PemFileWriter();
}
@AfterEach
void tearDown() throws IOException {
this.fileWriter.cleanup();
}
@Test
void parseCertificates() throws IOException {
Path caPath = this.fileWriter.writeFile("ca.pem", PemFileWriter.CA_CERTIFICATE);
Path certPath = this.fileWriter.writeFile("cert.pem", PemFileWriter.CERTIFICATE);
X509Certificate[] certificates = CertificateParser.parse(caPath, certPath);
assertThat(certificates).isNotNull();
assertThat(certificates.length).isEqualTo(2);
assertThat(certificates[0].getType()).isEqualTo("X.509");
assertThat(certificates[1].getType()).isEqualTo("X.509");
}
@Test
void parseCertificateChain() throws IOException {
Path path = this.fileWriter.writeFile("ca.pem", PemFileWriter.CA_CERTIFICATE, PemFileWriter.CERTIFICATE);
X509Certificate[] certificates = CertificateParser.parse(path);
assertThat(certificates).isNotNull();
assertThat(certificates.length).isEqualTo(2);
assertThat(certificates[0].getType()).isEqualTo("X.509");
assertThat(certificates[1].getType()).isEqualTo("X.509");
}
@Test
void parseWithInvalidPathWillThrowException() throws URISyntaxException {
Path path = Paths.get(new URI("file:///bad/path/cert.pem"));
assertThatIllegalStateException().isThrownBy(() -> CertificateParser.parse(path))
.withMessageContaining(path.toString());
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.docker.ssl;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link KeyStoreFactory}.
*
* @author Scott Frederick
*/
class KeyStoreFactoryTests {
private PemFileWriter fileWriter;
@BeforeEach
void setUp() throws IOException {
this.fileWriter = new PemFileWriter();
}
@AfterEach
void tearDown() throws IOException {
this.fileWriter.cleanup();
}
@Test
void createKeyStoreWithCertChain()
throws IOException, KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
Path certPath = this.fileWriter.writeFile("cert.pem", PemFileWriter.CA_CERTIFICATE, PemFileWriter.CERTIFICATE);
KeyStore keyStore = KeyStoreFactory.create(certPath, null, "test-alias");
assertThat(keyStore.containsAlias("test-alias-0")).isTrue();
assertThat(keyStore.getCertificate("test-alias-0")).isNotNull();
assertThat(keyStore.getKey("test-alias-0", new char[] {})).isNull();
assertThat(keyStore.containsAlias("test-alias-1")).isTrue();
assertThat(keyStore.getCertificate("test-alias-1")).isNotNull();
assertThat(keyStore.getKey("test-alias-1", new char[] {})).isNull();
Files.delete(certPath);
}
@Test
void createKeyStoreWithCertChainAndPrivateKey()
throws IOException, KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
Path certPath = this.fileWriter.writeFile("cert.pem", PemFileWriter.CA_CERTIFICATE, PemFileWriter.CERTIFICATE);
Path keyPath = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_KEY);
KeyStore keyStore = KeyStoreFactory.create(certPath, keyPath, "test-alias");
assertThat(keyStore.containsAlias("test-alias")).isTrue();
assertThat(keyStore.getCertificate("test-alias")).isNotNull();
assertThat(keyStore.getKey("test-alias", new char[] {})).isNotNull();
Files.delete(certPath);
Files.delete(keyPath);
}
}

View File

@@ -0,0 +1,122 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.docker.ssl;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import org.springframework.util.FileSystemUtils;
/**
* Utility to write certificate and key PEM files for testing.
*
* @author Scott Frederick
*/
public class PemFileWriter {
private static final String EXAMPLE_SECRET_QUALIFIER = "example";
public static final String CA_CERTIFICATE = "-----BEGIN TRUSTED CERTIFICATE-----\n"
+ "MIIClzCCAgACCQCPbjkRoMVEQDANBgkqhkiG9w0BAQUFADCBjzELMAkGA1UEBhMC\n"
+ "VVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28x\n"
+ "DTALBgNVBAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxFDASBgNVBAMMC2V4YW1wbGUu\n"
+ "Y29tMR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMB4XDTIwMDMyNzIx\n"
+ "NTgwNFoXDTIxMDMyNzIxNTgwNFowgY8xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApD\n"
+ "YWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQKDARUZXN0\n"
+ "MQ0wCwYDVQQLDARUZXN0MRQwEgYDVQQDDAtleGFtcGxlLmNvbTEfMB0GCSqGSIb3\n"
+ "DQEJARYQdGVzdEBleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkC\n"
+ "gYEA1YzixWEoyzrd20C2R1gjyPCoPfFLlG6UYTyT0tueNy6yjv6qbJ8lcZg7616O\n"
+ "3I9LuOHhZh9U+fCDCgPfiDdyJfDEW/P+dsOMFyMUXPrJPze2yPpOnvV8iJ5DM93u\n"
+ "fEVhCCyzLdYu0P2P3hU2W+T3/Im9DA7FOPA2vF1SrIJ2qtUCAwEAATANBgkqhkiG\n"
+ "9w0BAQUFAAOBgQBdShkwUv78vkn1jAdtfbB+7mpV9tufVdo29j7pmotTCz3ny5fc\n"
+ "zLEfeu6JPugAR71JYbc2CqGrMneSk1zT91EH6ohIz8OR5VNvzB7N7q65Ci7OFMPl\n"
+ "ly6k3rHpMCBtHoyNFhNVfPLxGJ9VlWFKLgIAbCmL4OIQm1l6Fr1MSM38Zw==\n" + "-----END TRUSTED CERTIFICATE-----\n";
public static final String CA_PRIVATE_KEY = EXAMPLE_SECRET_QUALIFIER + "-----BEGIN PRIVATE KEY-----\n"
+ "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANWM4sVhKMs63dtA\n"
+ "tkdYI8jwqD3xS5RulGE8k9Lbnjcuso7+qmyfJXGYO+tejtyPS7jh4WYfVPnwgwoD\n"
+ "34g3ciXwxFvz/nbDjBcjFFz6yT83tsj6Tp71fIieQzPd7nxFYQgssy3WLtD9j94V\n"
+ "Nlvk9/yJvQwOxTjwNrxdUqyCdqrVAgMBAAECgYEAyJTlZ8nj3Eg1nLxCue6C5jmN\n"
+ "fWkIuanH+zFAE/0utdxJ4WA4yYAOVo1MMr8FZwu9bzHTWe2yDnWnT5/ltPeHYX2X\n"
+ "9Pg5cY0tjq07utaMwLKWgJ0Xoh2UpVM799t/rSvMWmLaZ2c8nipX+gQfYJFpX8Vg\n"
+ "mR3QPxwdmNyFo13qif0CQQD4z2SqCfARuxscTCJDZ6wReikMQxaJvq74lPEtT26L\n"
+ "rBr/bN+mG7+rMEHxs5wtU47aNjUKuVVC0Qfhsf95ahvHAkEA27inSlxrwGvhvFsD\n"
+ "FWdgDsfYpPZdL4YgpVSEvcoypRGg2suJw2omcKcY56XpkmWUqZc06QirumtnEC0P\n"
+ "HfnsgwJBAMVhEURrOc13FxytsQiz96atuF6H4htH79o3ndQKDXI0B/7VSd6maLjP\n"
+ "QaESkTTL8qldE1r8h4zH8m6zHC4fZQUCQFWJ+8bdWC2fUlBr9jVc+26Fqvf92aVo\n"
+ "yEjVMKBamYDd7gt/9fAX4UM2KmH0m4wc89VaQoT+lSyMJ6GKiToYVFUCQEXcyoeO\n"
+ "zWqtSgEX/eXQXzmMKxYnjv1O//ba3Q7UiHd/XO5j4QXAJpcB6h0h00uC5KY2d0Zy\n" + "JQ1kB1C2l6l9tyc=\n"
+ "-----END PRIVATE KEY-----";
public static final String CERTIFICATE = "-----BEGIN CERTIFICATE-----\n"
+ "MIICjzCCAfgCAQEwDQYJKoZIhvcNAQEFBQAwgY8xCzAJBgNVBAYTAlVTMRMwEQYD\n"
+ "VQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMQ0wCwYDVQQK\n"
+ "DARUZXN0MQ0wCwYDVQQLDARUZXN0MRQwEgYDVQQDDAtleGFtcGxlLmNvbTEfMB0G\n"
+ "CSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTAeFw0yMDAzMjcyMjAxNDZaFw0y\n"
+ "MTAzMjcyMjAxNDZaMIGPMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5p\n"
+ "YTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwEVGVzdDENMAsGA1UE\n"
+ "CwwEVGVzdDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xHzAdBgkqhkiG9w0BCQEWEHRl\n"
+ "c3RAZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM7kd2cj\n"
+ "F49wm1+OQ7Q5GE96cXueWNPr/Nwei71tf6G4BmE0B+suXHEvnLpHTj9pdX/ZzBIK\n"
+ "8jIZ/x8RnSduK/Ky+zm1QMYUWZtWCAgCW8WzgB69Cn/hQG8KSX3S9bqODuQAvP54\n"
+ "GQJD7+4kVuNBGjFb4DaD4nvMmPtALSZf8ZCZAgMBAAEwDQYJKoZIhvcNAQEFBQAD\n"
+ "gYEAOn6X8+0VVlDjF+TvTgI0KIasA6nDm+KXe7LVtfvqWqQZH4qyd2uiwcDM3Aux\n"
+ "a/OsPdOw0j+NqFDBd3mSMhSVgfvXdK6j9WaxY1VGXyaidLARgvn63wfzgr857sQW\n"
+ "c8eSxbwEQxwlMvVxW6Os4VhCfUQr8VrBrvPa2zs+6IlK+Ug=\n" + "-----END CERTIFICATE-----\n";
public static final String PRIVATE_KEY = EXAMPLE_SECRET_QUALIFIER + "-----BEGIN RSA PRIVATE KEY-----\n"
+ "MIICXAIBAAKBgQDO5HdnIxePcJtfjkO0ORhPenF7nljT6/zcHou9bX+huAZhNAfr\n"
+ "LlxxL5y6R04/aXV/2cwSCvIyGf8fEZ0nbivysvs5tUDGFFmbVggIAlvFs4AevQp/\n"
+ "4UBvCkl90vW6jg7kALz+eBkCQ+/uJFbjQRoxW+A2g+J7zJj7QC0mX/GQmQIDAQAB\n"
+ "AoGAIWPsBWA7gDHrUYuzT5XbX5BiWlIfAezXPWtMoEDY1W/Oz8dG8+TilH3brJCv\n"
+ "hzps9TpgXhUYK4/Yhdog4+k6/EEY80RvcObOnflazTCVS041B0Ipm27uZjIq2+1F\n"
+ "ZfbWP+B3crpzh8wvIYA+6BCcZV9zi8Od32NEs39CtrOrFPUCQQDxnt9+JlWjtteR\n"
+ "VttRSKjtzKIF08BzNuZlRP9HNWveLhphIvdwBfjASwqgtuslqziEnGG8kniWzyYB\n"
+ "a/ZZVoT3AkEA2zSBMpvGPDkGbOMqbnR8UL3uijkOj+blQe1gsyu3dUa9T42O1u9h\n"
+ "Iz5SdCYlSFHbDNRFrwuW2QnhippqIQqC7wJAbVeyWEpM0yu5XiJqWdyB5iuG3xA2\n"
+ "tW0Q0p9ozvbT+9XtRiwmweFR8uOCybw9qexURV7ntAis3cKctmP/Neq7fQJBAKGa\n"
+ "59UjutYTRIVqRJICFtR/8ii9P9sfYs1j7/KnvC0d5duMhU44VOjivW8b4Eic8F1Y\n"
+ "8bbHWILSIhFJHg0V7skCQDa8/YkRWF/3pwIZNWQr4ce4OzvYsFMkRvGRdX8B2a0p\n"
+ "wSKcVTdEdO2DhBlYddN0zG0rjq4vDMtdmldEl4BdldQ=\n" + "-----END RSA PRIVATE KEY-----\n";
private final Path tempDir;
public PemFileWriter() throws IOException {
this.tempDir = Files.createTempDirectory("buildpack-platform-docker-ssl-tests");
}
Path writeFile(String name, String... contents) throws IOException {
Path path = Paths.get(this.tempDir.toString(), name);
for (String content : contents) {
Files.write(path, content.replaceAll(EXAMPLE_SECRET_QUALIFIER, "").getBytes(), StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
}
return path;
}
public Path getTempDir() {
return this.tempDir;
}
void cleanup() throws IOException {
FileSystemUtils.deleteRecursively(this.tempDir);
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.docker.ssl;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PrivateKey;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link PrivateKeyParser}.
*
* @author Scott Frederick
*/
class PrivateKeyParserTests {
private PemFileWriter fileWriter;
@BeforeEach
void setUp() throws IOException {
this.fileWriter = new PemFileWriter();
}
@AfterEach
void tearDown() throws IOException {
this.fileWriter.cleanup();
}
@Test
void parsePkcs8KeyFile() throws IOException {
Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.CA_PRIVATE_KEY);
PrivateKey privateKey = PrivateKeyParser.parse(path);
assertThat(privateKey).isNotNull();
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
Files.delete(path);
}
@Test
void parsePkcs1KeyFile() throws IOException {
Path path = this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_KEY);
PrivateKey privateKey = PrivateKeyParser.parse(path);
assertThat(privateKey).isNotNull();
// keys in PKCS#1 format are converted to PKCS#8 for parsing
assertThat(privateKey.getFormat()).isEqualTo("PKCS#8");
Files.delete(path);
}
@Test
void parseWithNonKeyFileWillThrowException() throws IOException {
Path path = this.fileWriter.writeFile("text.pem", "plain text");
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(path))
.withMessageContaining(path.toString());
Files.delete(path);
}
@Test
void parseWithInvalidPathWillThrowException() throws URISyntaxException {
URI privateKeyPath = new URI("file:///bad/path/key.pem");
assertThatIllegalStateException().isThrownBy(() -> PrivateKeyParser.parse(Paths.get(privateKeyPath)))
.withMessageContaining(privateKeyPath.getPath());
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.docker.ssl;
import java.io.IOException;
import javax.net.ssl.SSLContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SslContextFactory}.
*
* @author Scott Frederick
*/
class SslContextFactoryTests {
private PemFileWriter fileWriter;
@BeforeEach
void setUp() throws IOException {
this.fileWriter = new PemFileWriter();
}
@AfterEach
void tearDown() throws IOException {
this.fileWriter.cleanup();
}
@Test
void createKeyStoreWithCertChain() throws IOException {
this.fileWriter.writeFile("cert.pem", PemFileWriter.CERTIFICATE);
this.fileWriter.writeFile("key.pem", PemFileWriter.PRIVATE_KEY);
this.fileWriter.writeFile("ca.pem", PemFileWriter.CA_CERTIFICATE);
SSLContext sslContext = new SslContextFactory().forPath(this.fileWriter.getTempDir().toString());
assertThat(sslContext).isNotNull();
}
}