Merge pull request #22972 from wmz7year

* gh-22972:
  Polish "Support authentication to private Docker registry"
  Support authentication to private docker registry

Closes gh-22972
pull/23391/head
Scott Frederick 4 years ago
commit d1338a66f7

@ -24,6 +24,7 @@ import org.springframework.boot.buildpack.platform.docker.DockerApi;
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener; import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListener;
import org.springframework.boot.buildpack.platform.docker.UpdateListener; import org.springframework.boot.buildpack.platform.docker.UpdateListener;
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.transport.DockerEngineException;
import org.springframework.boot.buildpack.platform.docker.type.Image; import org.springframework.boot.buildpack.platform.docker.type.Image;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
@ -48,10 +49,18 @@ public class Builder {
this(BuildLog.toSystemOut()); this(BuildLog.toSystemOut());
} }
public Builder(DockerConfiguration dockerConfiguration) {
this(BuildLog.toSystemOut(), dockerConfiguration);
}
public Builder(BuildLog log) { public Builder(BuildLog log) {
this(log, new DockerApi()); this(log, new DockerApi());
} }
public Builder(BuildLog log, DockerConfiguration dockerConfiguration) {
this(log, new DockerApi(dockerConfiguration));
}
Builder(BuildLog log, DockerApi docker) { Builder(BuildLog log, DockerApi docker) {
Assert.notNull(log, "Log must not be null"); Assert.notNull(log, "Log must not be null");
this.log = log; this.log = log;

@ -26,6 +26,7 @@ import java.util.List;
import org.apache.http.client.utils.URIBuilder; import org.apache.http.client.utils.URIBuilder;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.transport.HttpTransport; 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.transport.HttpTransport.Response;
import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig; import org.springframework.boot.buildpack.platform.docker.type.ContainerConfig;
@ -68,7 +69,15 @@ public class DockerApi {
* Create a new {@link DockerApi} instance. * Create a new {@link DockerApi} instance.
*/ */
public DockerApi() { public DockerApi() {
this(HttpTransport.create()); this(DockerConfiguration.withDefaults());
}
/**
* Create a new {@link DockerApi} instance.
* @param dockerConfiguration the Docker configuration options
*/
public DockerApi(DockerConfiguration dockerConfiguration) {
this(HttpTransport.create(dockerConfiguration));
} }
/** /**

@ -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 org.springframework.util.Assert;
/**
* Docker configuration options.
*
* @author Wei Jiang
* @author Scott Frederick
* @since 2.4.0
*/
public final class DockerConfiguration {
private final DockerRegistryAuthentication authentication;
private DockerConfiguration(DockerRegistryAuthentication authentication) {
this.authentication = authentication;
}
public DockerRegistryAuthentication getRegistryAuthentication() {
return this.authentication;
}
public static DockerConfiguration withDefaults() {
return new DockerConfiguration(null);
}
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));
}
}

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

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

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

@ -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 configuration options.
*/
package org.springframework.boot.buildpack.platform.docker.configuration;

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

@ -22,6 +22,7 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URI; import java.net.URI;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.io.IOConsumer; import org.springframework.boot.buildpack.platform.io.IOConsumer;
import org.springframework.boot.buildpack.platform.system.Environment; import org.springframework.boot.buildpack.platform.system.Environment;
@ -84,7 +85,17 @@ public interface HttpTransport {
* @return a {@link HttpTransport} instance * @return a {@link HttpTransport} instance
*/ */
static HttpTransport create() { static HttpTransport create() {
return create(Environment.SYSTEM); return create(DockerConfiguration.withDefaults());
}
/**
* Create the most suitable {@link HttpTransport} based on the
* {@link Environment#SYSTEM system environment}.
* @param dockerConfiguration the Docker engine configuration
* @return a {@link HttpTransport} instance
*/
static HttpTransport create(DockerConfiguration dockerConfiguration) {
return create(Environment.SYSTEM, dockerConfiguration);
} }
/** /**
@ -94,8 +105,19 @@ public interface HttpTransport {
* @return a {@link HttpTransport} instance * @return a {@link HttpTransport} instance
*/ */
static HttpTransport create(Environment environment) { static HttpTransport create(Environment environment) {
HttpTransport remote = RemoteHttpClientTransport.createIfPossible(environment); return create(environment, DockerConfiguration.withDefaults());
return (remote != null) ? remote : LocalHttpClientTransport.create(environment); }
/**
* Create the most suitable {@link HttpTransport} based on the given
* {@link Environment} and {@link DockerConfiguration}.
* @param environment the source environment
* @param dockerConfiguration the Docker engine configuration
* @return a {@link HttpTransport} instance
*/
static HttpTransport create(Environment environment, DockerConfiguration dockerConfiguration) {
HttpTransport remote = RemoteHttpClientTransport.createIfPossible(environment, dockerConfiguration);
return (remote != null) ? remote : LocalHttpClientTransport.create(environment, dockerConfiguration);
} }
/** /**

@ -38,6 +38,7 @@ import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpContext;
import org.apache.http.util.Args; 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.DomainSocket;
import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket; import org.springframework.boot.buildpack.platform.socket.NamedPipeSocket;
import org.springframework.boot.buildpack.platform.system.Environment; import org.springframework.boot.buildpack.platform.system.Environment;
@ -56,15 +57,15 @@ final class LocalHttpClientTransport extends HttpClientTransport {
private static final HttpHost LOCAL_DOCKER_HOST = HttpHost.create("docker://localhost"); private static final HttpHost LOCAL_DOCKER_HOST = HttpHost.create("docker://localhost");
private LocalHttpClientTransport(CloseableHttpClient client) { private LocalHttpClientTransport(CloseableHttpClient client, DockerConfiguration dockerConfiguration) {
super(client, LOCAL_DOCKER_HOST); super(client, LOCAL_DOCKER_HOST, dockerConfiguration);
} }
static LocalHttpClientTransport create(Environment environment) { static LocalHttpClientTransport create(Environment environment, DockerConfiguration dockerConfiguration) {
HttpClientBuilder builder = HttpClients.custom(); HttpClientBuilder builder = HttpClients.custom();
builder.setConnectionManager(new LocalConnectionManager(socketFilePath(environment))); builder.setConnectionManager(new LocalConnectionManager(socketFilePath(environment)));
builder.setSchemePortResolver(new LocalSchemePortResolver()); builder.setSchemePortResolver(new LocalSchemePortResolver());
return new LocalHttpClientTransport(builder.build()); return new LocalHttpClientTransport(builder.build(), dockerConfiguration);
} }
private static String socketFilePath(Environment environment) { private static String socketFilePath(Environment environment) {

@ -28,6 +28,7 @@ import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients; 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.docker.ssl.SslContextFactory;
import org.springframework.boot.buildpack.platform.system.Environment; import org.springframework.boot.buildpack.platform.system.Environment;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -48,20 +49,23 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
private static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH"; private static final String DOCKER_CERT_PATH = "DOCKER_CERT_PATH";
private RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host) { private RemoteHttpClientTransport(CloseableHttpClient client, HttpHost host,
super(client, host); DockerConfiguration dockerConfiguration) {
super(client, host, dockerConfiguration);
} }
static RemoteHttpClientTransport createIfPossible(Environment environment) { static RemoteHttpClientTransport createIfPossible(Environment environment,
return createIfPossible(environment, new SslContextFactory()); DockerConfiguration dockerConfiguration) {
return createIfPossible(environment, dockerConfiguration, new SslContextFactory());
} }
static RemoteHttpClientTransport createIfPossible(Environment environment, SslContextFactory sslContextFactory) { static RemoteHttpClientTransport createIfPossible(Environment environment, DockerConfiguration dockerConfiguration,
SslContextFactory sslContextFactory) {
String host = environment.get(DOCKER_HOST); String host = environment.get(DOCKER_HOST);
if (host == null || isLocalFileReference(host)) { if (host == null || isLocalFileReference(host)) {
return null; return null;
} }
return create(environment, sslContextFactory, HttpHost.create(host)); return create(environment, sslContextFactory, HttpHost.create(host), dockerConfiguration);
} }
private static boolean isLocalFileReference(String host) { private static boolean isLocalFileReference(String host) {
@ -75,7 +79,7 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
} }
private static RemoteHttpClientTransport create(Environment environment, SslContextFactory sslContextFactory, private static RemoteHttpClientTransport create(Environment environment, SslContextFactory sslContextFactory,
HttpHost tcpHost) { HttpHost tcpHost, DockerConfiguration dockerConfiguration) {
HttpClientBuilder builder = HttpClients.custom(); HttpClientBuilder builder = HttpClients.custom();
boolean secure = isSecure(environment); boolean secure = isSecure(environment);
if (secure) { if (secure) {
@ -83,7 +87,7 @@ final class RemoteHttpClientTransport extends HttpClientTransport {
} }
String scheme = secure ? "https" : "http"; String scheme = secure ? "https" : "http";
HttpHost httpHost = new HttpHost(tcpHost.getHostName(), tcpHost.getPort(), scheme); 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, private static LayeredConnectionSocketFactory getSecureConnectionSocketFactory(Environment environment,

@ -60,7 +60,14 @@ class BuilderTests {
@Test @Test
void createWhenLogIsNullThrowsException() { void createWhenLogIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new Builder(null)).withMessage("Log must not be null"); assertThatIllegalArgumentException().isThrownBy(() -> new Builder((BuildLog) null))
.withMessage("Log must not be null");
}
@Test
void createWithDockerConfiguration() {
Builder builder = new Builder(BuildLog.toSystemOut());
assertThat(builder).isNotNull();
} }
@Test @Test

@ -113,6 +113,12 @@ class DockerApiTests {
}; };
} }
@Test
void createDockerApi() {
DockerApi api = new DockerApi();
assertThat(api).isNotNull();
}
@Nested @Nested
class ImageDockerApiTests { class ImageDockerApiTests {

@ -0,0 +1,61 @@
/*
* 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 static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DockerConfiguration}.
*
* @author Wei Jiang
* @author Scott Frederick
*/
public class DockerConfigurationTests {
@Test
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");
}
}

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

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

@ -22,6 +22,7 @@ import java.io.InputStream;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import org.apache.http.Header;
import org.apache.http.HttpEntity; import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpHeaders; import org.apache.http.HttpHeaders;
@ -43,7 +44,9 @@ import org.mockito.Captor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; 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.boot.buildpack.platform.docker.transport.HttpTransport.Response;
import org.springframework.util.Base64Utils;
import org.springframework.util.StreamUtils; import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -234,6 +237,47 @@ class HttpClientTransportTests {
.satisfies((ex) -> assertThat(ex.getMessage()).contains("test IO exception")); .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 { private String writeToString(HttpEntity entity) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteArrayOutputStream out = new ByteArrayOutputStream();
entity.writeTo(out); entity.writeTo(out);
@ -252,7 +296,11 @@ class HttpClientTransportTests {
static class TestHttpClientTransport extends HttpClientTransport { static class TestHttpClientTransport extends HttpClientTransport {
protected TestHttpClientTransport(CloseableHttpClient client) { 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);
} }
} }

@ -29,6 +29,7 @@ import org.apache.http.HttpHost;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; 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 org.springframework.boot.buildpack.platform.docker.ssl.SslContextFactory;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -46,9 +47,12 @@ class RemoteHttpClientTransportTests {
private final Map<String, String> environment = new LinkedHashMap<>(); private final Map<String, String> environment = new LinkedHashMap<>();
private final DockerConfiguration dockerConfiguration = DockerConfiguration.withDefaults();
@Test @Test
void createIfPossibleWhenDockerHostIsNotSetReturnsNull() { void createIfPossibleWhenDockerHostIsNotSetReturnsNull() {
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get); RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
this.dockerConfiguration);
assertThat(transport).isNull(); assertThat(transport).isNull();
} }
@ -57,14 +61,16 @@ class RemoteHttpClientTransportTests {
String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath() String dummySocketFilePath = Files.createTempFile(tempDir, "remote-transport", null).toAbsolutePath()
.toString(); .toString();
this.environment.put("DOCKER_HOST", dummySocketFilePath); 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(); assertThat(transport).isNull();
} }
@Test @Test
void createIfPossibleWhenDockerHostIsAddressReturnsTransport() { void createIfPossibleWhenDockerHostIsAddressReturnsTransport() {
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); 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).isNotNull(); assertThat(transport).isNotNull();
} }
@ -72,15 +78,16 @@ class RemoteHttpClientTransportTests {
void createIfPossibleWhenTlsVerifyWithMissingCertPathThrowsException() { void createIfPossibleWhenTlsVerifyWithMissingCertPathThrowsException() {
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376");
this.environment.put("DOCKER_TLS_VERIFY", "1"); this.environment.put("DOCKER_TLS_VERIFY", "1");
assertThatIllegalArgumentException() assertThatIllegalArgumentException().isThrownBy(
.isThrownBy(() -> RemoteHttpClientTransport.createIfPossible(this.environment::get)) () -> RemoteHttpClientTransport.createIfPossible(this.environment::get, this.dockerConfiguration))
.withMessageContaining("DOCKER_CERT_PATH"); .withMessageContaining("DOCKER_CERT_PATH");
} }
@Test @Test
void createIfPossibleWhenNoTlsVerifyUsesHttp() { void createIfPossibleWhenNoTlsVerifyUsesHttp() {
this.environment.put("DOCKER_HOST", "tcp://192.168.1.2:2376"); 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)); assertThat(transport.getHost()).satisfies(hostOf("http", "192.168.1.2", 2376));
} }
@ -92,10 +99,19 @@ class RemoteHttpClientTransportTests {
SslContextFactory sslContextFactory = mock(SslContextFactory.class); SslContextFactory sslContextFactory = mock(SslContextFactory.class);
given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault()); given(sslContextFactory.forDirectory("/test-cert-path")).willReturn(SSLContext.getDefault());
RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get, RemoteHttpClientTransport transport = RemoteHttpClientTransport.createIfPossible(this.environment::get,
sslContextFactory); this.dockerConfiguration, sslContextFactory);
assertThat(transport.getHost()).satisfies(hostOf("https", "192.168.1.2", 2376)); 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) { private Consumer<HttpHost> hostOf(String scheme, String hostName, int port) {
return (host) -> { return (host) -> {
assertThat(host).isNotNull(); assertThat(host).isNotNull();

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

@ -36,6 +36,37 @@ On Linux and macOS, these environment variables can be set using the command `ev
[[build-image-docker-registry]]
=== Docker Registry
If the Docker images specified by the `builder` or `runImage` parameters are stored in a private Docker image registry that requires authentication, the authentication credentials can be provided using `docker.registry` properties.
Properties are provided for user authentication or identity token authentication.
Consult the documentation for the Docker registry being used to store builder or run images for further information on supported authentication methods.
The following table summarizes the available properties:
|===
| Property | Description
| `username`
| Username for the Docker image registry user. Required for user authentication.
| `password`
| Password for the Docker image registry user. Required for user authentication.
| `url`
| Address of the Docker image registry. Optional for user authentication.
| `email`
| E-mail address for the Docker image registry user. Optional for user authentication.
| `token`
| Identity token for the Docker image registry user. Required for token authentication.
|===
For more details, see also <<build-image-example-docker,examples>>.
[[build-image-customization]] [[build-image-customization]]
=== Image Customizations === Image Customizations
The plugin invokes a {buildpacks-reference}/concepts/components/builder/[builder] to orchestrate the generation of an image. The plugin invokes a {buildpacks-reference}/concepts/components/builder/[builder] to orchestrate the generation of an image.
@ -186,3 +217,33 @@ The image name can be specified on the command line as well, as shown in this ex
---- ----
$ gradle bootBuildImage --imageName=example.com/library/my-app:v1 $ gradle bootBuildImage --imageName=example.com/library/my-app:v1
---- ----
[[build-image-example-docker]]
==== Docker Configuration
If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided as shown in the following example:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy
----
include::../gradle/packaging/boot-build-image-docker-auth-user.gradle[tags=docker-auth-user]
----
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
.Kotlin
----
include::../gradle/packaging/boot-build-image-docker-auth-user.gradle.kts[tags=docker-auth-user]
----
If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided as shown in the following example:
[source,groovy,indent=0,subs="verbatim,attributes",role="primary"]
.Groovy
----
include::../gradle/packaging/boot-build-image-docker-auth-token.gradle[tags=docker-auth-token]
----
[source,kotlin,indent=0,subs="verbatim,attributes",role="secondary"]
.Kotlin
----
include::../gradle/packaging/boot-build-image-docker-auth-token.gradle.kts[tags=docker-auth-token]
----

@ -0,0 +1,18 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{gradle-project-version}'
}
bootJar {
mainClassName 'com.example.ExampleApplication'
}
// tag::docker-auth-token[]
bootBuildImage {
docker {
registry {
token = "9cbaf023786cd7..."
}
}
}
// end::docker-auth-token[]

@ -0,0 +1,21 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
plugins {
java
id("org.springframework.boot") version "{gradle-project-version}"
}
tasks.getByName<BootJar>("bootJar") {
mainClassName = "com.example.ExampleApplication"
}
// tag::docker-auth-token[]
tasks.getByName<BootBuildImage>("bootBuildImage") {
docker {
registry {
token = "9cbaf023786cd7..."
}
}
}
// end::docker-auth-token[]

@ -0,0 +1,21 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{gradle-project-version}'
}
bootJar {
mainClassName 'com.example.ExampleApplication'
}
// tag::docker-auth-user[]
bootBuildImage {
docker {
registry {
username = "user"
password = "secret"
url = "https://docker.example.com/v1/"
email = "user@example.com"
}
}
}
// end::docker-auth-user[]

@ -0,0 +1,24 @@
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.bundling.BootBuildImage
plugins {
java
id("org.springframework.boot") version "{gradle-project-version}"
}
tasks.getByName<BootJar>("bootJar") {
mainClassName = "com.example.ExampleApplication"
}
// tag::docker-auth-user[]
tasks.getByName<BootBuildImage>("bootBuildImage") {
docker {
registry {
username = "user"
password = "secret"
url = "https://docker.example.com/v1/"
email = "user@example.com"
}
}
}
// end::docker-auth-user[]

@ -20,6 +20,8 @@ import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import groovy.lang.Closure;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask; import org.gradle.api.DefaultTask;
import org.gradle.api.JavaVersion; import org.gradle.api.JavaVersion;
import org.gradle.api.Project; import org.gradle.api.Project;
@ -27,9 +29,11 @@ import org.gradle.api.Task;
import org.gradle.api.file.RegularFileProperty; import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.provider.Property; import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.options.Option; import org.gradle.api.tasks.options.Option;
import org.gradle.util.ConfigureUtil;
import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.BuildRequest;
import org.springframework.boot.buildpack.platform.build.Builder; import org.springframework.boot.buildpack.platform.build.Builder;
@ -72,6 +76,8 @@ public class BootBuildImage extends DefaultTask {
private PullPolicy pullPolicy; private PullPolicy pullPolicy;
private DockerSpec docker = new DockerSpec();
public BootBuildImage() { public BootBuildImage() {
this.jar = getProject().getObjects().fileProperty(); this.jar = getProject().getObjects().fileProperty();
this.targetJavaVersion = getProject().getObjects().property(JavaVersion.class); this.targetJavaVersion = getProject().getObjects().property(JavaVersion.class);
@ -246,9 +252,37 @@ public class BootBuildImage extends DefaultTask {
this.pullPolicy = pullPolicy; this.pullPolicy = pullPolicy;
} }
/**
* Returns the Docker configuration the builder will use.
* @return docker configuration.
* @since 2.4.0
*/
@Nested
public DockerSpec getDocker() {
return this.docker;
}
/**
* Configures the Docker connection using the given {@code action}.
* @param action the action to apply
* @since 2.4.0
*/
public void docker(Action<DockerSpec> action) {
action.execute(this.docker);
}
/**
* Configures the Docker connection using the given {@code closure}.
* @param closure the closure to apply
* @since 2.4.0
*/
public void docker(Closure<?> closure) {
docker(ConfigureUtil.configureUsing(closure));
}
@TaskAction @TaskAction
void buildImage() throws DockerEngineException, IOException { void buildImage() throws DockerEngineException, IOException {
Builder builder = new Builder(); Builder builder = new Builder(this.docker.asDockerConfiguration());
BuildRequest request = createRequest(); BuildRequest request = createRequest();
builder.build(request); builder.build(request);
} }

@ -0,0 +1,214 @@
/*
* 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.gradle.tasks.bundling;
import groovy.lang.Closure;
import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.gradle.util.ConfigureUtil;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
/**
* Encapsulates Docker configuration options.
*
* @author Wei Jiang
* @author Scott Frederick
* @since 2.4.0
*/
public class DockerSpec {
private final DockerRegistrySpec registry;
public DockerSpec() {
this.registry = new DockerRegistrySpec();
}
DockerSpec(DockerRegistrySpec registry) {
this.registry = registry;
}
/**
* Returns the {@link DockerRegistrySpec} that configures registry authentication.
* @return the registry spec
*/
@Nested
public DockerRegistrySpec getRegistry() {
return this.registry;
}
/**
* Customizes the {@link DockerRegistrySpec} that configures registry authentication.
* @param action the action to apply
*/
public void registry(Action<DockerRegistrySpec> action) {
action.execute(this.registry);
}
/**
* Customizes the {@link DockerRegistrySpec} that configures registry authentication.
* @param closure the closure to apply
*/
public void registry(Closure<?> closure) {
registry(ConfigureUtil.configureUsing(closure));
}
/**
* Returns this configuration as a {@link DockerConfiguration} instance. This method
* should only be called when the configuration is complete and will no longer be
* changed.
* @return the Docker configuration
*/
DockerConfiguration asDockerConfiguration() {
if (this.registry == null || this.registry.hasEmptyAuth()) {
return null;
}
if (this.registry.hasTokenAuth() && !this.registry.hasUserAuth()) {
return DockerConfiguration.withRegistryTokenAuthentication(this.registry.getToken());
}
if (this.registry.hasUserAuth() && !this.registry.hasTokenAuth()) {
return DockerConfiguration.withRegistryUserAuthentication(this.registry.getUsername(),
this.registry.getPassword(), this.registry.getUrl(), this.registry.getEmail());
}
throw new GradleException(
"Invalid Docker registry configuration, either token or username/password must be provided");
}
/**
* Encapsulates Docker registry authentication configuration options.
*/
public static class DockerRegistrySpec {
private String username;
private String password;
private String url;
private String email;
private String token;
/**
* Returns the username to use when authenticating to the Docker registry.
* @return the registry username
*/
@Input
@Optional
public String getUsername() {
return this.username;
}
/**
* Sets the username to use when authenticating to the Docker registry.
* @param username the registry username
*/
public void setUsername(String username) {
this.username = username;
}
/**
* Returns the password to use when authenticating to the Docker registry.
* @return the registry password
*/
@Input
@Optional
public String getPassword() {
return this.password;
}
/**
* Sets the password to use when authenticating to the Docker registry.
* @param password the registry username
*/
public void setPassword(String password) {
this.password = password;
}
/**
* Returns the Docker registry URL.
* @return the registry URL
*/
@Input
@Optional
public String getUrl() {
return this.url;
}
/**
* Sets the Docker registry URL.
* @param url the registry URL
*/
public void setUrl(String url) {
this.url = url;
}
/**
* Returns the email address associated with the Docker registry username.
* @return the registry email address
*/
@Input
@Optional
public String getEmail() {
return this.email;
}
/**
* Sets the email address associated with the Docker registry username.
* @param email the registry email address
*/
public void setEmail(String email) {
this.email = email;
}
/**
* Returns the identity token to use when authenticating to the Docker registry.
* @return the registry identity token
*/
@Input
@Optional
public String getToken() {
return this.token;
}
/**
* Sets the identity token to use when authenticating to the Docker registry.
* @param token the registry identity token
*/
public void setToken(String token) {
this.token = token;
}
boolean hasEmptyAuth() {
return this.username == null && this.password == null && this.url == null && this.email == null
&& this.token == null;
}
boolean hasUserAuth() {
return this.getUsername() != null && this.getPassword() != null;
}
boolean hasTokenAuth() {
return this.getToken() != null;
}
}
}

@ -0,0 +1,100 @@
/*
* 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.gradle.tasks.bundling;
import org.gradle.api.GradleException;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
import org.springframework.util.Base64Utils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link DockerSpec}.
*
* @author Wei Jiang
* @author Scott Frederick
*/
public class DockerSpecTests {
@Test
void asDockerConfigurationWithoutRegistry() {
DockerSpec dockerSpec = new DockerSpec();
assertThat(dockerSpec.asDockerConfiguration()).isNull();
}
@Test
void asDockerConfigurationWithEmptyRegistry() {
DockerSpec dockerSpec = new DockerSpec(new DockerSpec.DockerRegistrySpec());
assertThat(dockerSpec.asDockerConfiguration()).isNull();
}
@Test
void asDockerConfigurationWithUserAuth() {
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
dockerRegistry.setUsername("user");
dockerRegistry.setPassword("secret");
dockerRegistry.setUrl("https://docker.example.com");
dockerRegistry.setEmail("docker@example.com");
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration();
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
assertThat(registryAuthentication).isNotNull();
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
.contains("\"username\" : \"user\"").contains("\"password\" : \"secret\"")
.contains("\"email\" : \"docker@example.com\"")
.contains("\"serveraddress\" : \"https://docker.example.com\"");
}
@Test
void asDockerConfigurationWithIncompleteUserAuthFails() {
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
dockerRegistry.setUsername("user");
dockerRegistry.setUrl("https://docker.example.com");
dockerRegistry.setEmail("docker@example.com");
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
assertThatExceptionOfType(GradleException.class).isThrownBy(dockerSpec::asDockerConfiguration)
.withMessageContaining("Invalid Docker registry configuration");
}
@Test
void asDockerConfigurationWithTokenAuth() {
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
dockerRegistry.setToken("token");
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
DockerConfiguration dockerConfiguration = dockerSpec.asDockerConfiguration();
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
assertThat(registryAuthentication).isNotNull();
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
.contains("\"identitytoken\" : \"token\"");
}
@Test
void asDockerConfigurationWithUserAndTokenAuthFails() {
DockerSpec.DockerRegistrySpec dockerRegistry = new DockerSpec.DockerRegistrySpec();
dockerRegistry.setUsername("user");
dockerRegistry.setPassword("secret");
dockerRegistry.setToken("token");
DockerSpec dockerSpec = new DockerSpec(dockerRegistry);
assertThatExceptionOfType(GradleException.class).isThrownBy(dockerSpec::asDockerConfiguration)
.withMessageContaining("Invalid Docker registry configuration");
}
}

@ -59,6 +59,37 @@ On Linux and macOS, these environment variables can be set using the command `ev
[[build-image-docker-registry]]
=== Docker Registry
If the Docker images specified by the `builder` or `runImage` parameters are stored in a private Docker image registry that requires authentication, the authentication credentials can be provided using `docker.registry` parameters.
Parameters are provided for user authentication or identity token authentication.
Consult the documentation for the Docker registry being used to store builder or run images for further information on supported authentication methods.
The following table summarizes the available parameters:
|===
| Parameter | Description
| `username`
| Username for the Docker image registry user. Required for user authentication.
| `password`
| Password for the Docker image registry user. Required for user authentication.
| `url`
| Address of the Docker image registry. Optional for user authentication.
| `email`
| E-mail address for the Docker image registry user. Optional for user authentication.
| `token`
| Identity token for the Docker image registry user. Required for token authentication.
|===
For more details, see also <<build-image-example-docker,examples>>.
[[build-image-customization]] [[build-image-customization]]
=== Image Customizations === Image Customizations
The plugin invokes a {buildpacks-reference}/concepts/components/builder/[builder] to orchestrate the generation of an image. The plugin invokes a {buildpacks-reference}/concepts/components/builder/[builder] to orchestrate the generation of an image.
@ -252,3 +283,57 @@ The image name can be specified on the command line as well, as shown in this ex
$ mvn spring-boot:build-image -Dspring-boot.build-image.imageName=example.com/library/my-app:v1 $ mvn spring-boot:build-image -Dspring-boot.build-image.imageName=example.com/library/my-app:v1
---- ----
[[build-image-example-docker]]
==== Docker Configuration
If the builder or run image are stored in a private Docker registry that supports user authentication, authentication details can be provided as shown in the following example:
[source,xml,indent=0,subs="verbatim,attributes"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<docker>
<registry>
<username>user</username>
<password>secret</password>
<url>https://docker.example.com/v1/</url>
<email>user@example.com</email>
</registry>
</docker>
</configuration>
</plugin>
</plugins>
</build>
</project>
----
If the builder or run image is stored in a private Docker registry that supports token authentication, the token value can be provided as shown in the following example:
[source,xml,indent=0,subs="verbatim,attributes"]
----
<project>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>{gradle-project-version}</version>
<configuration>
<docker>
<registry>
<token>9cbaf023786cd7...</token>
</registry>
</docker>
</configuration>
</plugin>
</plugins>
</build>
</project>
----

@ -44,6 +44,7 @@ import org.springframework.boot.buildpack.platform.build.Builder;
import org.springframework.boot.buildpack.platform.build.Creator; import org.springframework.boot.buildpack.platform.build.Creator;
import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.build.PullPolicy;
import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent; import org.springframework.boot.buildpack.platform.docker.TotalProgressEvent;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.Owner;
import org.springframework.boot.buildpack.platform.io.TarArchive; import org.springframework.boot.buildpack.platform.io.TarArchive;
import org.springframework.boot.loader.tools.EntryWriter; import org.springframework.boot.loader.tools.EntryWriter;
@ -135,6 +136,13 @@ public class BuildImageMojo extends AbstractPackagerMojo {
@Parameter(property = "spring-boot.build-image.pullPolicy", readonly = true) @Parameter(property = "spring-boot.build-image.pullPolicy", readonly = true)
PullPolicy pullPolicy; PullPolicy pullPolicy;
/**
* Docker configuration options.
* @since 2.4.0
*/
@Parameter
private Docker docker;
@Override @Override
public void execute() throws MojoExecutionException { public void execute() throws MojoExecutionException {
if (this.project.getPackaging().equals("pom")) { if (this.project.getPackaging().equals("pom")) {
@ -151,7 +159,9 @@ public class BuildImageMojo extends AbstractPackagerMojo {
private void buildImage() throws MojoExecutionException { private void buildImage() throws MojoExecutionException {
Libraries libraries = getLibraries(Collections.emptySet()); Libraries libraries = getLibraries(Collections.emptySet());
try { try {
Builder builder = new Builder(new MojoBuildLog(this::getLog)); DockerConfiguration dockerConfiguration = (this.docker != null) ? this.docker.asDockerConfiguration()
: null;
Builder builder = new Builder(new MojoBuildLog(this::getLog), dockerConfiguration);
BuildRequest request = getBuildRequest(libraries); BuildRequest request = getBuildRequest(libraries);
builder.build(request); builder.build(request);
} }

@ -0,0 +1,132 @@
/*
* 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.maven;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
/**
* Docker configuration options.
*
* @author Wei Jiang
* @author Scott Frederick
* @since 2.4.0
*/
public class Docker {
private DockerRegistry registry;
/**
* Sets the {@link DockerRegistry} that configures registry authentication.
* @param registry the registry configuration
*/
public void setRegistry(DockerRegistry registry) {
this.registry = registry;
}
/**
* Returns this configuration as a {@link DockerConfiguration} instance. This method
* should only be called when the configuration is complete and will no longer be
* changed.
* @return the Docker configuration
*/
DockerConfiguration asDockerConfiguration() {
if (this.registry == null || this.registry.isEmpty()) {
return null;
}
if (this.registry.hasTokenAuth() && !this.registry.hasUserAuth()) {
return DockerConfiguration.withRegistryTokenAuthentication(this.registry.getToken());
}
if (this.registry.hasUserAuth() && !this.registry.hasTokenAuth()) {
return DockerConfiguration.withRegistryUserAuthentication(this.registry.getUsername(),
this.registry.getPassword(), this.registry.getUrl(), this.registry.getEmail());
}
throw new IllegalArgumentException(
"Invalid Docker registry configuration, either token or username/password must be provided");
}
/**
* Encapsulates Docker registry authentication configuration options.
*/
public static class DockerRegistry {
private String username;
private String password;
private String url;
private String email;
private String token;
String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
String getEmail() {
return this.email;
}
public void setEmail(String email) {
this.email = email;
}
String getUrl() {
return this.url;
}
public void setUrl(String url) {
this.url = url;
}
String getToken() {
return this.token;
}
public void setToken(String token) {
this.token = token;
}
boolean isEmpty() {
return this.username == null && this.password == null && this.url == null && this.email == null
&& this.token == null;
}
boolean hasTokenAuth() {
return this.token != null;
}
boolean hasUserAuth() {
return this.username != null && this.password != null;
}
}
}

@ -0,0 +1,105 @@
/*
* 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.maven;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
import org.springframework.util.Base64Utils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link Docker}.
*
* @author Wei Jiang
* @author Scott Frederick
*/
public class DockerTests {
@Test
void asDockerConfigurationWithoutRegistry() {
Docker docker = new Docker();
assertThat(docker.asDockerConfiguration()).isNull();
}
@Test
void asDockerConfigurationWithEmptyRegistry() {
Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry();
Docker docker = new Docker();
docker.setRegistry(dockerRegistry);
assertThat(docker.asDockerConfiguration()).isNull();
}
@Test
void asDockerConfigurationWithUserAuth() {
Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry();
dockerRegistry.setUsername("user");
dockerRegistry.setPassword("secret");
dockerRegistry.setUrl("https://docker.example.com");
dockerRegistry.setEmail("docker@example.com");
Docker docker = new Docker();
docker.setRegistry(dockerRegistry);
DockerConfiguration dockerConfiguration = docker.asDockerConfiguration();
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
assertThat(registryAuthentication).isNotNull();
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
.contains("\"username\" : \"user\"").contains("\"password\" : \"secret\"")
.contains("\"email\" : \"docker@example.com\"")
.contains("\"serveraddress\" : \"https://docker.example.com\"");
}
@Test
void asDockerConfigurationWithIncompleteUserAuthFails() {
Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry();
dockerRegistry.setUsername("user");
dockerRegistry.setUrl("https://docker.example.com");
dockerRegistry.setEmail("docker@example.com");
Docker docker = new Docker();
docker.setRegistry(dockerRegistry);
assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration)
.withMessageContaining("Invalid Docker registry configuration");
}
@Test
void asDockerConfigurationWithTokenAuth() {
Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry();
dockerRegistry.setToken("token");
Docker docker = new Docker();
docker.setRegistry(dockerRegistry);
DockerConfiguration dockerConfiguration = docker.asDockerConfiguration();
DockerRegistryAuthentication registryAuthentication = dockerConfiguration.getRegistryAuthentication();
assertThat(registryAuthentication).isNotNull();
assertThat(new String(Base64Utils.decodeFromString(registryAuthentication.createAuthHeader())))
.contains("\"identitytoken\" : \"token\"");
}
@Test
void asDockerConfigurationWithUserAndTokenAuthFails() {
Docker.DockerRegistry dockerRegistry = new Docker.DockerRegistry();
dockerRegistry.setUsername("user");
dockerRegistry.setPassword("secret");
dockerRegistry.setToken("token");
Docker docker = new Docker();
docker.setRegistry(dockerRegistry);
assertThatIllegalArgumentException().isThrownBy(docker::asDockerConfiguration)
.withMessageContaining("Invalid Docker registry configuration");
}
}
Loading…
Cancel
Save