Polish "Support authentication to private Docker registry"

See gh-22972
This commit is contained in:
Scott Frederick
2020-08-24 14:10:01 -05:00
parent e8f555e13d
commit 86fa8144f5
40 changed files with 1193 additions and 747 deletions

View File

@@ -54,7 +54,7 @@ public class Builder {
}
public Builder(BuildLog log) {
this(log, new DockerApi(new DockerConfiguration()));
this(log, new DockerApi());
}
public Builder(BuildLog log, DockerConfiguration dockerConfiguration) {

View File

@@ -24,12 +24,9 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.apache.http.Header;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.message.BasicHeader;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryConfiguration;
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;
@@ -72,15 +69,15 @@ public class DockerApi {
* Create a new {@link DockerApi} instance.
*/
public DockerApi() {
this(new DockerConfiguration());
this(DockerConfiguration.withDefaults());
}
/**
* Create a new {@link DockerApi} instance.
* @param dockerConfiguration the Docker configuration options.
* @param dockerConfiguration the Docker configuration options
*/
public DockerApi(DockerConfiguration dockerConfiguration) {
this(HttpTransport.create(createDockerEngineAuthenticationHeaders(dockerConfiguration)));
this(HttpTransport.create(dockerConfiguration));
}
/**
@@ -96,22 +93,6 @@ public class DockerApi {
this.volume = new VolumeApi();
}
static Collection<Header> createDockerEngineAuthenticationHeaders(DockerConfiguration dockerConfiguration) {
Assert.notNull(dockerConfiguration, "Docker configuration must not be null");
DockerRegistryConfiguration dockerRegistryConfiguration = dockerConfiguration.getDockerRegistryConfiguration();
if (dockerRegistryConfiguration == null) {
return Collections.emptyList();
}
String dockerRegistryAuthToken = dockerRegistryConfiguration.createDockerRegistryAuthToken();
if (StringUtils.isEmpty(dockerRegistryAuthToken)) {
return Collections.emptyList();
}
return Arrays.asList(new BasicHeader("X-Registry-Auth", dockerRegistryAuthToken));
}
private HttpTransport http() {
return this.http;
}

View File

@@ -16,34 +16,41 @@
package org.springframework.boot.buildpack.platform.docker.configuration;
import org.springframework.util.Assert;
/**
* Docker configuration options.
*
* @author Wei Jiang
* @author Scott Frederick
* @since 2.4.0
*/
public class DockerConfiguration {
public final class DockerConfiguration {
/**
* The docker registry configuration.
*/
private DockerRegistryConfiguration dockerRegistryConfiguration;
private final DockerRegistryAuthentication authentication;
public DockerConfiguration() {
super();
private DockerConfiguration(DockerRegistryAuthentication authentication) {
this.authentication = authentication;
}
public DockerConfiguration(DockerRegistryConfiguration dockerRegistryConfiguration) {
super();
this.dockerRegistryConfiguration = dockerRegistryConfiguration;
public DockerRegistryAuthentication getRegistryAuthentication() {
return this.authentication;
}
public DockerRegistryConfiguration getDockerRegistryConfiguration() {
return this.dockerRegistryConfiguration;
public static DockerConfiguration withDefaults() {
return new DockerConfiguration(null);
}
public void setDockerRegistryConfiguration(DockerRegistryConfiguration dockerRegistryConfiguration) {
this.dockerRegistryConfiguration = dockerRegistryConfiguration;
public static DockerConfiguration withRegistryTokenAuthentication(String token) {
Assert.notNull(token, "Token must not be null");
return new DockerConfiguration(new DockerRegistryTokenAuthentication(token));
}
public static DockerConfiguration withRegistryUserAuthentication(String username, String password, String url,
String email) {
Assert.notNull(username, "Username must not be null");
Assert.notNull(password, "Password must not be null");
return new DockerConfiguration(new DockerRegistryUserAuthentication(username, password, url, email));
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.configuration;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
import org.springframework.util.Base64Utils;
/**
* Docker registry authentication configuration.
*
* @author Scott Frederick
* @since 2.4.0
*/
public abstract class DockerRegistryAuthentication {
public String createAuthHeader() {
try {
return Base64Utils.encodeToUrlSafeString(SharedObjectMapper.get().writeValueAsBytes(this));
}
catch (JsonProcessingException ex) {
throw new IllegalStateException("Error creating Docker registry authentication header", ex);
}
}
}

View File

@@ -1,129 +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.configuration;
import java.io.IOException;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
import org.springframework.util.Base64Utils;
import org.springframework.util.StringUtils;
/**
* Docker registry configuration options.
*
* @author Wei Jiang
* @since 2.4.0
*/
public class DockerRegistryConfiguration {
/**
* Docker registry server address.
*/
@JsonProperty("serveraddress")
private String url;
/**
* Docker registry authentication username.
*/
private String username;
/**
* Docker registry authentication password.
*/
private String password;
/**
* Docker registry authentication email.
*/
private String email;
/**
* Docker registry authentication identity token.
*/
@JsonIgnore
private String token;
public DockerRegistryConfiguration() {
super();
}
public DockerRegistryConfiguration(String url, String username, String password, String email, String token) {
super();
this.url = url;
this.username = username;
this.password = password;
this.email = email;
this.token = token;
}
public String createDockerRegistryAuthToken() {
if (!StringUtils.isEmpty(this.getToken())) {
return this.getToken();
}
try {
return Base64Utils.encodeToString(SharedObjectMapper.get().writeValueAsBytes(this));
}
catch (IOException ex) {
throw new IllegalStateException("create docker registry authentication token failed.", ex);
}
}
public String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
public String getToken() {
return this.token;
}
public void setToken(String token) {
this.token = token;
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Docker registry authentication configuration using a token.
*
* @author Scott Frederick
*/
class DockerRegistryTokenAuthentication extends DockerRegistryAuthentication {
@JsonProperty("identitytoken")
private final String token;
DockerRegistryTokenAuthentication(String token) {
this.token = token;
}
String getToken() {
return this.token;
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Docker registry authentication configuration using user credentials.
*
* @author Scott Frederick
*/
class DockerRegistryUserAuthentication extends DockerRegistryAuthentication {
@JsonProperty
private final String username;
@JsonProperty
private final String password;
@JsonProperty("serveraddress")
private final String url;
@JsonProperty
private final String email;
DockerRegistryUserAuthentication(String username, String password, String url, String email) {
this.username = username;
this.password = password;
this.url = url;
this.email = email;
}
String getUsername() {
return this.username;
}
String getPassword() {
return this.password;
}
String getUrl() {
return this.url;
}
String getEmail() {
return this.email;
}
}

View File

@@ -36,10 +36,12 @@ 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.configuration.DockerConfiguration;
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;
import org.springframework.util.StringUtils;
/**
* Abstract base class for {@link HttpTransport} implementations backed by a
@@ -55,11 +57,14 @@ abstract class HttpClientTransport implements HttpTransport {
private final HttpHost host;
protected HttpClientTransport(CloseableHttpClient client, HttpHost host) {
private final String registryAuthHeader;
protected HttpClientTransport(CloseableHttpClient client, HttpHost host, DockerConfiguration dockerConfiguration) {
Assert.notNull(client, "Client must not be null");
Assert.notNull(host, "Host must not be null");
this.client = client;
this.host = host;
this.registryAuthHeader = buildRegistryAuthHeader(dockerConfiguration);
}
/**
@@ -116,6 +121,15 @@ abstract class HttpClientTransport implements HttpTransport {
return execute(new HttpDelete(uri));
}
private String buildRegistryAuthHeader(DockerConfiguration dockerConfiguration) {
if (dockerConfiguration == null || dockerConfiguration.getRegistryAuthentication() == null) {
return null;
}
String authHeader = dockerConfiguration.getRegistryAuthentication().createAuthHeader();
return (StringUtils.hasText(authHeader)) ? authHeader : null;
}
private Response execute(HttpEntityEnclosingRequestBase request, String contentType,
IOConsumer<OutputStream> writer) {
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
@@ -125,6 +139,9 @@ abstract class HttpClientTransport implements HttpTransport {
private Response execute(HttpUriRequest request) {
try {
if (this.registryAuthHeader != null) {
request.addHeader("X-Registry-Auth", this.registryAuthHeader);
}
CloseableHttpResponse response = this.client.execute(this.host, request);
StatusLine statusLine = response.getStatusLine();
int statusCode = statusLine.getStatusCode();

View File

@@ -21,11 +21,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Collection;
import java.util.Collections;
import org.apache.http.Header;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.boot.buildpack.platform.system.Environment;
@@ -88,17 +85,17 @@ public interface HttpTransport {
* @return a {@link HttpTransport} instance
*/
static HttpTransport create() {
return create(Collections.emptyList());
return create(DockerConfiguration.withDefaults());
}
/**
* Create the most suitable {@link HttpTransport} based on the
* {@link Environment#SYSTEM system environment}.
* @param dockerEngineAuthenticationHeaders authentication headerS for Docker engine.
* @param dockerConfiguration the Docker engine configuration
* @return a {@link HttpTransport} instance
*/
static HttpTransport create(Collection<Header> dockerEngineAuthenticationHeaders) {
return create(Environment.SYSTEM, dockerEngineAuthenticationHeaders);
static HttpTransport create(DockerConfiguration dockerConfiguration) {
return create(Environment.SYSTEM, dockerConfiguration);
}
/**
@@ -108,21 +105,19 @@ public interface HttpTransport {
* @return a {@link HttpTransport} instance
*/
static HttpTransport create(Environment environment) {
return create(environment, Collections.emptyList());
return create(environment, DockerConfiguration.withDefaults());
}
/**
* Create the most suitable {@link HttpTransport} based on the given
* {@link Environment}.
* {@link Environment} and {@link DockerConfiguration}.
* @param environment the source environment
* @param dockerEngineAuthenticationHeaders authentication headerS for Docker engine.
* @param dockerConfiguration the Docker engine configuration
* @return a {@link HttpTransport} instance
*/
static HttpTransport create(Environment environment, Collection<Header> dockerEngineAuthenticationHeaders) {
HttpTransport remote = RemoteHttpClientTransport.createIfPossible(environment,
dockerEngineAuthenticationHeaders);
return (remote != null) ? remote
: LocalHttpClientTransport.create(environment, dockerEngineAuthenticationHeaders);
static HttpTransport create(Environment environment, DockerConfiguration dockerConfiguration) {
HttpTransport remote = RemoteHttpClientTransport.createIfPossible(environment, dockerConfiguration);
return (remote != null) ? remote : LocalHttpClientTransport.create(environment, dockerConfiguration);
}
/**

View File

@@ -21,10 +21,8 @@ import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.Collection;
import com.sun.jna.Platform;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
@@ -40,10 +38,10 @@ 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.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.socket.DomainSocket;
import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket;
import org.springframework.boot.buildpack.platform.system.Environment;
import org.springframework.util.CollectionUtils;
/**
* {@link HttpClientTransport} that talks to local Docker.
@@ -59,19 +57,15 @@ 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);
private LocalHttpClientTransport(CloseableHttpClient client, DockerConfiguration dockerConfiguration) {
super(client, LOCAL_DOCKER_HOST, dockerConfiguration);
}
static LocalHttpClientTransport create(Environment environment,
Collection<Header> dockerEngineAuthenticationHeaders) {
static LocalHttpClientTransport create(Environment environment, DockerConfiguration dockerConfiguration) {
HttpClientBuilder builder = HttpClients.custom();
builder.setConnectionManager(new LocalConnectionManager(socketFilePath(environment)));
builder.setSchemePortResolver(new LocalSchemePortResolver());
if (!CollectionUtils.isEmpty(dockerEngineAuthenticationHeaders)) {
builder.setDefaultHeaders(dockerEngineAuthenticationHeaders);
}
return new LocalHttpClientTransport(builder.build());
return new LocalHttpClientTransport(builder.build(), dockerConfiguration);
}
private static String socketFilePath(Environment environment) {

View File

@@ -18,12 +18,9 @@ package org.springframework.boot.buildpack.platform.docker.transport;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import javax.net.ssl.SSLContext;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
@@ -31,10 +28,10 @@ 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.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory;
import org.springframework.boot.buildpack.platform.system.Environment;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* {@link HttpClientTransport} that talks to a remote Docker.
@@ -52,30 +49,23 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
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, Collections.emptyList());
private RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host,
DockerConfiguration dockerConfiguration) {
super(client, host, dockerConfiguration);
}
static RemoteHttpClientTransport createIfPossible(Environment environment,
Collection<Header> dockerEngineAuthenticationHeaders) {
return createIfPossible(environment, new SslContextFactory(), dockerEngineAuthenticationHeaders);
DockerConfiguration dockerConfiguration) {
return createIfPossible(environment, dockerConfiguration, new SslContextFactory());
}
static RemoteHttpClientTransport createIfPossible(Environment environment, SslContextFactory sslContextFactory) {
return createIfPossible(environment, sslContextFactory, Collections.emptyList());
}
static RemoteHttpClientTransport createIfPossible(Environment environment, SslContextFactory sslContextFactory,
Collection<Header> dockerEngineAuthenticationHeaders) {
static RemoteHttpClientTransport createIfPossible(Environment environment, DockerConfiguration dockerConfiguration,
SslContextFactory sslContextFactory) {
String host = environment.get(DOCKER_HOST);
if (host == null || isLocalFileReference(host)) {
return null;
}
return create(environment, sslContextFactory, HttpHost.create(host), dockerEngineAuthenticationHeaders);
return create(environment, sslContextFactory, HttpHost.create(host), dockerConfiguration);
}
private static boolean isLocalFileReference(String host) {
@@ -89,18 +79,15 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
}
private static RemoteHttpClientTransport create(Environment environment, SslContextFactory sslContextFactory,
HttpHost tcpHost, Collection<Header> dockerEngineAuthenticationHeaders) {
HttpHost tcpHost, DockerConfiguration dockerConfiguration) {
HttpClientBuilder builder = HttpClients.custom();
boolean secure = isSecure(environment);
if (secure) {
builder.setSSLSocketFactory(getSecureConnectionSocketFactory(environment, sslContextFactory));
}
if (!CollectionUtils.isEmpty(dockerEngineAuthenticationHeaders)) {
builder.setDefaultHeaders(dockerEngineAuthenticationHeaders);
}
String scheme = secure ? "https" : "http";
HttpHost httpHost = new HttpHost(tcpHost.getHostName(), tcpHost.getPort(), scheme);
return new RemoteHttpClientTransport(builder.build(), httpHost);
return new RemoteHttpClientTransport(builder.build(), httpHost, dockerConfiguration);
}
private static LayeredConnectionSocketFactory getSecureConnectionSocketFactory(Environment environment,

View File

@@ -30,7 +30,6 @@ 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.TotalProgressPullListener;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
import org.springframework.boot.buildpack.platform.docker.type.ContainerReference;
import org.springframework.boot.buildpack.platform.docker.type.ContainerStatus;
@@ -67,7 +66,7 @@ class BuilderTests {
@Test
void createWithDockerConfiguration() {
Builder builder = new Builder(BuildLog.toSystemOut(), new DockerConfiguration());
Builder builder = new Builder(BuildLog.toSystemOut());
assertThat(builder).isNotNull();
}

View File

@@ -20,9 +20,7 @@ import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.util.Collection;
import org.apache.http.Header;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -36,8 +34,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
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.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryConfiguration;
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;
@@ -52,7 +48,6 @@ import org.springframework.boot.buildpack.platform.io.Content;
import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.util.Base64Utils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -124,40 +119,6 @@ class DockerApiTests {
assertThat(api).isNotNull();
}
@Test
void createDockerApiWithDockerConfiguration() {
DockerApi api = new DockerApi(new DockerConfiguration());
assertThat(api).isNotNull();
}
@Test
void createWhenDockerConfigurationIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new DockerApi((DockerConfiguration) null))
.withMessage("Docker configuration must not be null");
}
@Test
void createDockerEngineAuthenticationHeaders() {
DockerRegistryConfiguration dockerRegistryConfiguration = new DockerRegistryConfiguration();
dockerRegistryConfiguration.setUsername("username");
dockerRegistryConfiguration.setPassword("password");
dockerRegistryConfiguration.setEmail("mock@spring.com");
dockerRegistryConfiguration.setUrl("http://mock.docker.registry");
DockerConfiguration dockerConfiguration = new DockerConfiguration();
dockerConfiguration.setDockerRegistryConfiguration(dockerRegistryConfiguration);
Collection<Header> dockerEngineAuthenticationHeaders = DockerApi
.createDockerEngineAuthenticationHeaders(dockerConfiguration);
assertThat(dockerEngineAuthenticationHeaders.size() == 1).isTrue();
Header header = dockerEngineAuthenticationHeaders.iterator().next();
assertThat(header.getName()).isEqualTo("X-Registry-Auth");
assertThat(header.getValue()).isEqualTo(
"ewogICJ1c2VybmFtZSIgOiAidXNlcm5hbWUiLAogICJwYXNzd29yZCIgOiAicGFzc3dvcmQiLAogICJlbWFpbCIgOiAibW9ja0BzcHJpbmcuY29tIiwKICAic2VydmVyYWRkcmVzcyIgOiAiaHR0cDovL21vY2suZG9ja2VyLnJlZ2lzdHJ5Igp9");
assertThat(new String(Base64Utils.decodeFromString(header.getValue())))
.isEqualTo("{\n" + " \"username\" : \"username\",\n" + " \"password\" : \"password\",\n"
+ " \"email\" : \"mock@spring.com\",\n"
+ " \"serveraddress\" : \"http://mock.docker.registry\"\n" + "}");
}
@Nested
class ImageDockerApiTests {

View File

@@ -24,12 +24,38 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link DockerConfiguration}.
*
* @author Wei Jiang
* @author Scott Frederick
*/
public class DockerConfigurationTests {
@Test
void createDockerConfiguration() {
assertThat(new DockerConfiguration()).isNotNull();
void createDockerConfigurationWithDefaults() {
DockerConfiguration configuration = DockerConfiguration.withDefaults();
assertThat(configuration.getRegistryAuthentication()).isNull();
}
@Test
void createDockerConfigurationWithUserAuth() {
DockerConfiguration configuration = DockerConfiguration.withRegistryUserAuthentication("user", "secret",
"https://docker.example.com", "docker@example.com");
DockerRegistryAuthentication auth = configuration.getRegistryAuthentication();
assertThat(auth).isNotNull();
assertThat(auth).isInstanceOf(DockerRegistryUserAuthentication.class);
DockerRegistryUserAuthentication userAuth = (DockerRegistryUserAuthentication) auth;
assertThat(userAuth.getUrl()).isEqualTo("https://docker.example.com");
assertThat(userAuth.getUsername()).isEqualTo("user");
assertThat(userAuth.getPassword()).isEqualTo("secret");
assertThat(userAuth.getEmail()).isEqualTo("docker@example.com");
}
@Test
void createDockerConfigurationWithTokenAuth() {
DockerConfiguration configuration = DockerConfiguration.withRegistryTokenAuthentication("token");
DockerRegistryAuthentication auth = configuration.getRegistryAuthentication();
assertThat(auth).isNotNull();
assertThat(auth).isInstanceOf(DockerRegistryTokenAuthentication.class);
DockerRegistryTokenAuthentication tokenAuth = (DockerRegistryTokenAuthentication) auth;
assertThat(tokenAuth.getToken()).isEqualTo("token");
}
}

View File

@@ -1,79 +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.configuration;
import org.junit.jupiter.api.Test;
import org.springframework.util.Base64Utils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DockerRegistryConfiguration}.
*
* @author Wei Jiang
*/
public class DockerRegistryConfigurationTests {
@Test
void createDockerRegistryAuthTokenWithToken() {
DockerRegistryConfiguration dockerRegistryConfiguration = new DockerRegistryConfiguration();
dockerRegistryConfiguration.setToken("mockToken");
assertThat(dockerRegistryConfiguration.createDockerRegistryAuthToken()).isEqualTo("mockToken");
}
@Test
void createDockerRegistryAuthTokenWithoutToken() {
DockerRegistryConfiguration dockerRegistryConfiguration = new DockerRegistryConfiguration();
dockerRegistryConfiguration.setUsername("username");
dockerRegistryConfiguration.setPassword("password");
dockerRegistryConfiguration.setEmail("mock@spring.com");
dockerRegistryConfiguration.setUrl("http://mock.docker.registry");
String token = dockerRegistryConfiguration.createDockerRegistryAuthToken();
assertThat(token).isEqualTo(
"ewogICJ1c2VybmFtZSIgOiAidXNlcm5hbWUiLAogICJwYXNzd29yZCIgOiAicGFzc3dvcmQiLAogICJlbWFpbCIgOiAibW9ja0BzcHJpbmcuY29tIiwKICAic2VydmVyYWRkcmVzcyIgOiAiaHR0cDovL21vY2suZG9ja2VyLnJlZ2lzdHJ5Igp9");
assertThat(new String(Base64Utils.decodeFromString(token))).isEqualTo("{\n" + " \"username\" : \"username\",\n"
+ " \"password\" : \"password\",\n" + " \"email\" : \"mock@spring.com\",\n"
+ " \"serveraddress\" : \"http://mock.docker.registry\"\n" + "}");
}
@Test
void createDockerRegistryAuthTokenWithUsernameAndPassword() {
DockerRegistryConfiguration dockerRegistryConfiguration = new DockerRegistryConfiguration();
dockerRegistryConfiguration.setUsername("username");
dockerRegistryConfiguration.setPassword("password");
String token = dockerRegistryConfiguration.createDockerRegistryAuthToken();
assertThat(dockerRegistryConfiguration.getEmail()).isNull();
assertThat(dockerRegistryConfiguration.getUrl()).isNull();
assertThat(token).isEqualTo(
"ewogICJ1c2VybmFtZSIgOiAidXNlcm5hbWUiLAogICJwYXNzd29yZCIgOiAicGFzc3dvcmQiLAogICJlbWFpbCIgOiBudWxsLAogICJzZXJ2ZXJhZGRyZXNzIiA6IG51bGwKfQ==");
assertThat(new String(Base64Utils.decodeFromString(token))).isEqualTo("{\n" + " \"username\" : \"username\",\n"
+ " \"password\" : \"password\",\n" + " \"email\" : null,\n" + " \"serveraddress\" : null\n" + "}");
}
@Test
void createDockerRegistryAuthTokenWithTokenAndUsername() {
DockerRegistryConfiguration dockerRegistryConfiguration = new DockerRegistryConfiguration();
dockerRegistryConfiguration.setToken("mockToken");
dockerRegistryConfiguration.setUsername("username");
dockerRegistryConfiguration.setPassword("password");
dockerRegistryConfiguration.setEmail("mock@spring.com");
dockerRegistryConfiguration.setUrl("http://mock.docker.registry");
assertThat(dockerRegistryConfiguration.createDockerRegistryAuthToken()).isEqualTo("mockToken");
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.buildpack.platform.docker.configuration;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import org.springframework.util.Base64Utils;
import org.springframework.util.StreamUtils;
/**
* Tests for {@link DockerRegistryTokenAuthentication}.
*/
class DockerRegistryTokenAuthenticationTests extends AbstractJsonTests {
@Test
void createAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
DockerRegistryTokenAuthentication auth = new DockerRegistryTokenAuthentication("tokenvalue");
String header = auth.createAuthHeader();
String expectedJson = StreamUtils.copyToString(getContent("auth-token.json"), StandardCharsets.UTF_8);
JSONAssert.assertEquals(expectedJson, new String(Base64Utils.decodeFromUrlSafeString(header)), false);
}
}

View File

@@ -0,0 +1,56 @@
/*
* 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.configuration;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import org.springframework.util.Base64Utils;
import org.springframework.util.StreamUtils;
/**
* Tests for {@link DockerRegistryUserAuthentication}.
*/
class DockerRegistryUserAuthenticationTests extends AbstractJsonTests {
@Test
void createMinimalAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
DockerRegistryUserAuthentication auth = new DockerRegistryUserAuthentication("user", "secret",
"https://docker.example.com", "docker@example.com");
JSONAssert.assertEquals(jsonContent("auth-user-full.json"), decoded(auth.createAuthHeader()), false);
}
@Test
void createFullAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
DockerRegistryUserAuthentication auth = new DockerRegistryUserAuthentication("user", "secret", null, null);
JSONAssert.assertEquals(jsonContent("auth-user-minimal.json"), decoded(auth.createAuthHeader()), false);
}
private String jsonContent(String s) throws IOException {
return StreamUtils.copyToString(getContent(s), StandardCharsets.UTF_8);
}
private String decoded(String header) {
return new String(Base64Utils.decodeFromUrlSafeString(header));
}
}

View File

@@ -22,6 +22,7 @@ import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHeaders;
@@ -43,7 +44,9 @@ import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport.Response;
import org.springframework.util.Base64Utils;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -234,6 +237,47 @@ class HttpClientTransportTests {
.satisfies((ex) -> assertThat(ex.getMessage()).contains("test IO exception"));
}
@Test
void getWithDockerRegistryUserAuthWillSendAuthHeader() throws IOException {
DockerConfiguration dockerConfiguration = DockerConfiguration.withRegistryUserAuthentication("user", "secret",
"https://docker.example.com", "docker@example.com");
this.http = new TestHttpClientTransport(this.client, dockerConfiguration);
givenClientWillReturnResponse();
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.hostCaptor.capture(), this.requestCaptor.capture());
HttpUriRequest request = this.requestCaptor.getValue();
assertThat(request).isInstanceOf(HttpGet.class);
assertThat(request.getURI()).isEqualTo(this.uri);
Header[] registryAuthHeaders = request.getHeaders("X-Registry-Auth");
assertThat(registryAuthHeaders).isNotNull();
assertThat(new String(Base64Utils.decodeFromString(registryAuthHeaders[0].getValue())))
.contains("\"username\" : \"user\"").contains("\"password\" : \"secret\"")
.contains("\"email\" : \"docker@example.com\"")
.contains("\"serveraddress\" : \"https://docker.example.com\"");
assertThat(response.getContent()).isSameAs(this.content);
}
@Test
void getWithDockerRegistryTokenAuthWillSendAuthHeader() throws IOException {
DockerConfiguration dockerConfiguration = DockerConfiguration.withRegistryTokenAuthentication("token");
this.http = new TestHttpClientTransport(this.client, dockerConfiguration);
givenClientWillReturnResponse();
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.hostCaptor.capture(), this.requestCaptor.capture());
HttpUriRequest request = this.requestCaptor.getValue();
assertThat(request).isInstanceOf(HttpGet.class);
assertThat(request.getURI()).isEqualTo(this.uri);
Header[] registryAuthHeaders = request.getHeaders("X-Registry-Auth");
assertThat(registryAuthHeaders).isNotNull();
assertThat(new String(Base64Utils.decodeFromString(registryAuthHeaders[0].getValue())))
.contains("\"identitytoken\" : \"token\"");
assertThat(response.getContent()).isSameAs(this.content);
}
private String writeToString(HttpEntity entity) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
entity.writeTo(out);
@@ -252,7 +296,11 @@ class HttpClientTransportTests {
static class TestHttpClientTransport extends HttpClientTransport {
protected TestHttpClientTransport(CloseableHttpClient client) {
super(client, HttpHost.create("docker://localhost"));
super(client, HttpHost.create("docker://localhost"), null);
}
protected TestHttpClientTransport(CloseableHttpClient client, DockerConfiguration dockerConfiguration) {
super(client, HttpHost.create("docker://localhost"), dockerConfiguration);
}
}

View File

@@ -19,13 +19,9 @@ package org.springframework.boot.buildpack.platform.docker.transport;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import org.apache.http.Header;
import org.apache.http.message.BasicHeader;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -69,12 +65,4 @@ class HttpTransportTests {
assertThat(transport).isInstanceOf(LocalHttpClientTransport.class);
}
@Test
void createWithDockerEngineAuthenticationHeaders() {
Collection<Header> dockerEngineAuthenticationHeaders = Arrays.asList(new BasicHeader("X-Registry-Auth",
"eyJ1c2VybmFtZSI6ICJ1c2VybmFtZSIsInBhc3N3b3JkIjogInBhc3N3b3JkIiwiZW1haWwiOiAibW9ja0BzcHJpbmcuY29tIiwic2VydmVyYWRkcmVzcyI6ICJodHRwOi8vbW9jay5kb2NrZXIucmVnaXN0cnkifQ=="));
HttpTransport transport = HttpTransport.create((name) -> null, dockerEngineAuthenticationHeaders);
assertThat(transport).isInstanceOf(LocalHttpClientTransport.class);
}
}

View File

@@ -19,20 +19,17 @@ package org.springframework.boot.buildpack.platform.docker.transport;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import javax.net.ssl.SSLContext;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.message.BasicHeader;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory;
import static org.assertj.core.api.Assertions.assertThat;
@@ -50,9 +47,12 @@ class RemoteHttpClientTransportTests {
private final Map<String, String> environment = new LinkedHashMap<>();
private final DockerConfiguration dockerConfiguration = DockerConfiguration.withDefaults();
@Test
void createIfPossibleWhenDockerHostIsNotSetReturnsNull() {
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get);
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
this.dockerConfiguration);
assertThat(transport).isNull();
}
@@ -61,24 +61,16 @@ class RemoteHttpClientTransportTests {
String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath()
.toString();
this.environment.put("DOCKER_HOST", dummySocketFilePath);
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get);
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
this.dockerConfiguration);
assertThat(transport).isNull();
}
@Test
void createIfPossibleWhenDockerHostIsAddressReturnsTransport() {
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get);
assertThat(transport).isNotNull();
}
@Test
void createWithDockerEngineAuthenticationHeaders() {
Collection<Header> dockerEngineAuthenticationHeaders = Arrays.asList(new BasicHeader("X-Registry-Auth",
"eyJ1c2VybmFtZSI6ICJ1c2VybmFtZSIsInBhc3N3b3JkIjogInBhc3N3b3JkIiwiZW1haWwiOiAibW9ja0BzcHJpbmcuY29tIiwic2VydmVyYWRkcmVzcyI6ICJodHRwOi8vbW9jay5kb2NrZXIucmVnaXN0cnkifQ=="));
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
dockerEngineAuthenticationHeaders);
this.dockerConfiguration);
assertThat(transport).isNotNull();
}
@@ -86,15 +78,16 @@ class RemoteHttpClientTransportTests {
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))
assertThatIllegalArgumentException().isThrownBy(
() -> RemoteHttpClientTransport.createIfPossible(this.environment::get, this.dockerConfiguration))
.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);
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
this.dockerConfiguration);
assertThat(transport.getHost()).satisfies(hostOf("http", "192.168.1.2", 2376));
}
@@ -106,10 +99,19 @@ class RemoteHttpClientTransportTests {
SslContextFactory sslContextFactory = mock(SslContextFactory.class);
given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault());
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
sslContextFactory);
this.dockerConfiguration, sslContextFactory);
assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376));
}
@Test
void createIfPossibleWithDockerConfigurationUserAuthReturnsTransport() {
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
DockerConfiguration.withRegistryUserAuthentication("user", "secret", "http://docker.example.com",
"docker@example.com"));
assertThat(transport).isNotNull();
}
private Consumer<HttpHost> hostOf(String scheme, String hostName, int port) {
return (host) -> {
assertThat(host).isNotNull();

View File

@@ -0,0 +1,6 @@
{
"username": "user",
"password": "secret",
"email": "docker@example.com",
"serveraddress": "https://docker.example.com"
}