Browse Source

Merge pull request #45269 from nosan

* pr/45269:
  Add Docker configuration authentication to Maven and Gradle plugins
  Support Docker configuration authentication including helper support
  Polish 'Update `DockerConfigurationMetadata` to support credentials'
  Update `DockerConfigurationMetadata` to support credentials

Closes gh-45269
pull/45286/head
Phillip Webb 9 months ago
parent
commit
e50da4fca2
  1. 10
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java
  2. 68
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/Credential.java
  3. 103
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java
  4. 87
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java
  5. 45
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java
  6. 150
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryConfigAuthentication.java
  7. 5
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryTokenAuthentication.java
  8. 5
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryUserAuthentication.java
  9. 22
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java
  10. 98
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java
  11. 90
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialsTests.java
  12. 41
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java
  13. 357
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryConfigAuthenticationTests.java
  14. 39
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/docker-credential-test.bat
  15. 43
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/docker-credential-test.sh
  16. 21
      spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-auth/config.json
  17. 12
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc
  18. 9
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java
  19. 12
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java
  20. 12
      spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc
  21. 6
      spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java
  22. 26
      spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java
  23. 18
      spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java

10
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java

@ -246,13 +246,13 @@ public class Builder { @@ -246,13 +246,13 @@ public class Builder {
private void pushImage(ImageReference reference) throws IOException {
Consumer<TotalProgressEvent> progressConsumer = this.log.pushingImage(reference);
TotalProgressPushListener listener = new TotalProgressPushListener(progressConsumer);
String authHeader = authHeader(this.dockerConfiguration.publishRegistryAuthentication());
String authHeader = authHeader(this.dockerConfiguration.publishRegistryAuthentication(), reference);
this.docker.image().push(reference, listener, authHeader);
this.log.pushedImage(reference);
}
private static String authHeader(DockerRegistryAuthentication authentication) {
return (authentication != null) ? authentication.getAuthHeader() : null;
private static String authHeader(DockerRegistryAuthentication authentication, ImageReference reference) {
return (authentication != null) ? authentication.getAuthHeader(reference) : null;
}
/**
@ -279,7 +279,7 @@ public class Builder { @@ -279,7 +279,7 @@ public class Builder {
Image fetchImage(ImageType type, ImageReference reference) throws IOException {
Assert.notNull(type, "'type' must not be null");
Assert.notNull(reference, "'reference' must not be null");
String authHeader = authHeader(this.registryAuthentication);
String authHeader = authHeader(this.registryAuthentication, reference);
Assert.state(authHeader == null || reference.getDomain().equals(this.domain),
() -> String.format("%s '%s' must be pulled from the '%s' authenticated registry",
StringUtils.capitalize(type.getDescription()), reference, this.domain));
@ -300,7 +300,7 @@ public class Builder { @@ -300,7 +300,7 @@ public class Builder {
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
TotalProgressPullListener listener = new TotalProgressPullListener(
Builder.this.log.pullingImage(reference, this.defaultPlatform, imageType));
String authHeader = authHeader(this.registryAuthentication);
String authHeader = authHeader(this.registryAuthentication, reference);
Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, authHeader);
Builder.this.log.pulledImage(image, imageType);
if (this.defaultPlatform == null) {

68
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/Credential.java

@ -0,0 +1,68 @@ @@ -0,0 +1,68 @@
/*
* Copyright 2012-2025 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.lang.invoke.MethodHandles;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.boot.buildpack.platform.json.MappedObject;
/**
* A class that represents credentials for a server as returned from a
* {@link CredentialHelper}.
*
* @author Dmytro Nosan
*/
class Credential extends MappedObject {
/**
* If the secret being stored is an identity token, the username should be set to
* {@code <token>}.
*/
private static final String TOKEN_USERNAME = "<token>";
private final String username;
private final String secret;
private String serverUrl;
Credential(JsonNode node) {
super(node, MethodHandles.lookup());
this.username = valueAt("/Username", String.class);
this.secret = valueAt("/Secret", String.class);
this.serverUrl = valueAt("/ServerURL", String.class);
}
String getUsername() {
return this.username;
}
String getSecret() {
return this.secret;
}
String getServerUrl() {
return this.serverUrl;
}
boolean isIdentityToken() {
return TOKEN_USERNAME.equals(this.username);
}
}

103
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java

@ -0,0 +1,103 @@ @@ -0,0 +1,103 @@
/*
* Copyright 2012-2025 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.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import com.sun.jna.Platform;
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
/**
* Invokes a Docker credential helper executable that can be used to get {@link Credential
* credentials}.
*
* @author Dmytro Nosan
* @author Phillip Webb
*/
class CredentialHelper {
private static final String USR_LOCAL_BIN = "/usr/local/bin/";
Set<String> CREDENTIAL_NOT_FOUND_MESSAGES = Set.of("credentials not found in native keychain",
"no credentials server URL", "no credentials username");
private final String executable;
CredentialHelper(String executable) {
this.executable = executable;
}
Credential get(String serverUrl) throws IOException {
ProcessBuilder processBuilder = processBuilder("get");
Process process = start(processBuilder);
try (OutputStream request = process.getOutputStream()) {
request.write(serverUrl.getBytes(StandardCharsets.UTF_8));
}
try {
int exitCode = process.waitFor();
try (InputStream response = process.getInputStream()) {
if (exitCode == 0) {
return new Credential(SharedObjectMapper.get().readTree(response));
}
String errorMessage = new String(response.readAllBytes(), StandardCharsets.UTF_8);
if (!isCredentialsNotFoundError(errorMessage)) {
throw new IOException("%s' exited with code %d: %s".formatted(process, exitCode, errorMessage));
}
return null;
}
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return null;
}
}
private ProcessBuilder processBuilder(String string) {
ProcessBuilder processBuilder = new ProcessBuilder().redirectErrorStream(true);
if (Platform.isWindows()) {
processBuilder.command("cmd", "/c");
}
processBuilder.command(this.executable, string);
return processBuilder;
}
private Process start(ProcessBuilder processBuilder) throws IOException {
try {
return processBuilder.start();
}
catch (IOException ex) {
if (!Platform.isMac()) {
throw ex;
}
List<String> command = new ArrayList<>(processBuilder.command());
command.set(0, USR_LOCAL_BIN + command.get(0));
return processBuilder.command(command).start();
}
}
private boolean isCredentialsNotFoundError(String message) {
return this.CREDENTIAL_NOT_FOUND_MESSAGES.contains(message.trim());
}
}

87
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 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.
@ -23,7 +23,9 @@ import java.nio.file.Files; @@ -23,7 +23,9 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HexFormat;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
@ -32,11 +34,14 @@ import com.fasterxml.jackson.databind.node.NullNode; @@ -32,11 +34,14 @@ import com.fasterxml.jackson.databind.node.NullNode;
import org.springframework.boot.buildpack.platform.json.MappedObject;
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
import org.springframework.boot.buildpack.platform.system.Environment;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Docker configuration stored in metadata files managed by the Docker CLI.
*
* @author Scott Frederick
* @author Dmytro Nosan
*/
final class DockerConfigurationMetadata {
@ -58,6 +63,8 @@ final class DockerConfigurationMetadata { @@ -58,6 +63,8 @@ final class DockerConfigurationMetadata {
private static final String CONTEXT_FILE_NAME = "meta.json";
private static volatile DockerConfigurationMetadata systemEnvironmentConfigurationMetadata;
private final String configLocation;
private final DockerConfig config;
@ -83,11 +90,24 @@ final class DockerConfigurationMetadata { @@ -83,11 +90,24 @@ final class DockerConfigurationMetadata {
}
static DockerConfigurationMetadata from(Environment environment) {
String configLocation = (environment.get(DOCKER_CONFIG) != null) ? environment.get(DOCKER_CONFIG)
: Path.of(System.getProperty("user.home"), CONFIG_DIR).toString();
DockerConfigurationMetadata dockerConfigurationMetadata = (environment == Environment.SYSTEM)
? DockerConfigurationMetadata.systemEnvironmentConfigurationMetadata : null;
if (dockerConfigurationMetadata != null) {
return dockerConfigurationMetadata;
}
String configLocation = environment.get(DOCKER_CONFIG);
configLocation = (configLocation != null) ? configLocation : getUserHomeConfigLocation();
DockerConfig dockerConfig = createDockerConfig(configLocation);
DockerContext dockerContext = createDockerContext(configLocation, dockerConfig.getCurrentContext());
return new DockerConfigurationMetadata(configLocation, dockerConfig, dockerContext);
dockerConfigurationMetadata = new DockerConfigurationMetadata(configLocation, dockerConfig, dockerContext);
if (environment == Environment.SYSTEM) {
DockerConfigurationMetadata.systemEnvironmentConfigurationMetadata = dockerConfigurationMetadata;
}
return dockerConfigurationMetadata;
}
private static String getUserHomeConfigLocation() {
return Path.of(System.getProperty("user.home"), CONFIG_DIR).toString();
}
private static DockerConfig createDockerConfig(String configLocation) {
@ -148,15 +168,36 @@ final class DockerConfigurationMetadata { @@ -148,15 +168,36 @@ final class DockerConfigurationMetadata {
private final String currentContext;
private final String credsStore;
private final Map<String, String> credHelpers;
private final Map<String, Auth> auths;
private DockerConfig(JsonNode node) {
super(node, MethodHandles.lookup());
this.currentContext = valueAt("/currentContext", String.class);
this.credsStore = valueAt("/credsStore", String.class);
this.credHelpers = mapAt("/credHelpers", JsonNode::textValue);
this.auths = mapAt("/auths", Auth::new);
}
String getCurrentContext() {
return this.currentContext;
}
String getCredsStore() {
return this.credsStore;
}
Map<String, String> getCredHelpers() {
return this.credHelpers;
}
Map<String, Auth> getAuths() {
return this.auths;
}
static DockerConfig fromJson(String json) throws JsonProcessingException {
return new DockerConfig(SharedObjectMapper.get().readTree(json));
}
@ -167,6 +208,44 @@ final class DockerConfigurationMetadata { @@ -167,6 +208,44 @@ final class DockerConfigurationMetadata {
}
static final class Auth extends MappedObject {
private final String username;
private final String password;
private final String email;
Auth(JsonNode node) {
super(node, MethodHandles.lookup());
String auth = valueAt("/auth", String.class);
if (StringUtils.hasText(auth)) {
String[] parts = new String(Base64.getDecoder().decode(auth)).split(":", 2);
Assert.state(parts.length == 2, "Malformed auth in docker configuration metadata");
this.username = parts[0];
this.password = parts[1];
}
else {
this.username = valueAt("/username", String.class);
this.password = valueAt("/password", String.class);
}
this.email = valueAt("/email", String.class);
}
String getUsername() {
return this.username;
}
String getPassword() {
return this.password;
}
String getEmail() {
return this.email;
}
}
static final class DockerContext extends MappedObject {
private final String dockerHost;

45
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java

@ -16,12 +16,17 @@ @@ -16,12 +16,17 @@
package org.springframework.boot.buildpack.platform.docker.configuration;
import java.util.function.BiConsumer;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
/**
* Docker registry authentication configuration.
*
* @author Scott Frederick
* @since 2.4.0
*/
@FunctionalInterface
public interface DockerRegistryAuthentication {
/**
@ -30,6 +35,17 @@ public interface DockerRegistryAuthentication { @@ -30,6 +35,17 @@ public interface DockerRegistryAuthentication {
*/
DockerRegistryAuthentication EMPTY_USER = DockerRegistryAuthentication.user("", "", "", "");
/**
* Returns the auth header that should be used for docker authentication for the given
* image reference.
* @param imageReference the image reference or {@code null}
* @return the auth header
* @since 3.5.0
*/
default String getAuthHeader(ImageReference imageReference) {
return getAuthHeader();
}
/**
* Returns the auth header that should be used for docker authentication.
* @return the auth header
@ -63,4 +79,33 @@ public interface DockerRegistryAuthentication { @@ -63,4 +79,33 @@ public interface DockerRegistryAuthentication {
return new DockerRegistryUserAuthentication(username, password, serverAddress, email);
}
/**
* Factory method that returns a new {@link DockerRegistryAuthentication} instance
* that uses the standard docker JSON config (including support for credential
* helpers) to generate auth headers.
* @param fallback the fallback authentication to use if no suitable config is found
* @return a new {@link DockerRegistryAuthentication} instance
* @since 3.5.0
* @see #configuration(DockerRegistryAuthentication, BiConsumer)
*/
static DockerRegistryAuthentication configuration(DockerRegistryAuthentication fallback) {
return configuration(fallback, (message, ex) -> System.out.println(message));
}
/**
* Factory method that returns a new {@link DockerRegistryAuthentication} instance
* that uses the standard docker JSON config (including support for credential
* helpers) to generate auth headers.
* @param fallback the fallback authentication to use if no suitable config is found
* @param credentialHelperExceptionHandler callback that should handle credential
* helper exceptions
* @return a new {@link DockerRegistryAuthentication} instance
* @since 3.5.0
* @see #configuration(DockerRegistryAuthentication, BiConsumer)
*/
static DockerRegistryAuthentication configuration(DockerRegistryAuthentication fallback,
BiConsumer<String, Exception> credentialHelperExceptionHandler) {
return new DockerRegistryConfigAuthentication(fallback, credentialHelperExceptionHandler);
}
}

150
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryConfigAuthentication.java

@ -0,0 +1,150 @@ @@ -0,0 +1,150 @@
/*
* Copyright 2012-2025 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.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.Auth;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.DockerConfig;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.system.Environment;
import org.springframework.util.StringUtils;
/**
* {@link DockerRegistryAuthentication} for
* {@link DockerRegistryAuthentication#configuration(DockerRegistryAuthentication, BiConsumer)}.
*
* @author Dmytro Nosan
* @author Phillip Webb
*/
class DockerRegistryConfigAuthentication implements DockerRegistryAuthentication {
private static final String DEFAULT_DOMAIN = "docker.io";
private static final String INDEX_URL = "https://index.docker.io/v1/";
private static Map<String, Credential> credentialFromHelperCache = new ConcurrentHashMap<>();
private final DockerRegistryAuthentication fallback;
private final BiConsumer<String, Exception> credentialHelperExceptionHandler;
private final Function<String, CredentialHelper> credentialHelperFactory;
private final DockerConfig dockerConfig;
DockerRegistryConfigAuthentication(DockerRegistryAuthentication fallback,
BiConsumer<String, Exception> credentialHelperExceptionHandler) {
this(fallback, credentialHelperExceptionHandler, Environment.SYSTEM,
(helper) -> new CredentialHelper("docker-credential-" + helper.trim()));
}
DockerRegistryConfigAuthentication(DockerRegistryAuthentication fallback,
BiConsumer<String, Exception> credentialHelperExceptionHandler, Environment environment,
Function<String, CredentialHelper> credentialHelperFactory) {
this.fallback = fallback;
this.credentialHelperExceptionHandler = credentialHelperExceptionHandler;
this.dockerConfig = DockerConfigurationMetadata.from(environment).getConfiguration();
this.credentialHelperFactory = credentialHelperFactory;
}
@Override
public String getAuthHeader() {
return getAuthHeader(null);
}
@Override
public String getAuthHeader(ImageReference imageReference) {
String serverUrl = getServerUrl(imageReference);
DockerRegistryAuthentication authentication = getAuthentication(serverUrl);
return (authentication != null) ? authentication.getAuthHeader(imageReference) : null;
}
private String getServerUrl(ImageReference imageReference) {
String domain = (imageReference != null) ? imageReference.getDomain() : null;
return (!DEFAULT_DOMAIN.equals(domain)) ? domain : INDEX_URL;
}
private DockerRegistryAuthentication getAuthentication(String serverUrl) {
Credential credentialsFromHelper = getCredentialsFromHelper(serverUrl);
Map.Entry<String, Auth> authConfigEntry = getAuthConfigEntry(serverUrl);
serverUrl = (authConfigEntry != null) ? authConfigEntry.getKey() : serverUrl;
Auth authConfig = (authConfigEntry != null) ? authConfigEntry.getValue() : null;
if (credentialsFromHelper != null) {
return getAuthentication(credentialsFromHelper, authConfig, serverUrl);
}
if (authConfigEntry != null) {
return DockerRegistryAuthentication.user(authConfig.getUsername(), authConfig.getPassword(), serverUrl,
authConfig.getEmail());
}
return this.fallback;
}
private DockerRegistryAuthentication getAuthentication(Credential credentialsFromHelper, Auth authConfig,
String serverUrl) {
if (credentialsFromHelper.isIdentityToken()) {
return DockerRegistryAuthentication.token(credentialsFromHelper.getSecret());
}
String username = credentialsFromHelper.getUsername();
String password = credentialsFromHelper.getSecret();
String serverAddress = (credentialsFromHelper.getServerUrl() != null
&& !credentialsFromHelper.getServerUrl().isEmpty()) ? credentialsFromHelper.getServerUrl() : serverUrl;
String email = (authConfig != null) ? authConfig.getEmail() : null;
return DockerRegistryAuthentication.user(username, password, serverAddress, email);
}
private Credential getCredentialsFromHelper(String serverUrl) {
return (StringUtils.hasText(serverUrl))
? credentialFromHelperCache.computeIfAbsent(serverUrl, this::computeCredentialsFromHelper) : null;
}
private Credential computeCredentialsFromHelper(String serverUrl) {
CredentialHelper credentialHelper = getCredentialHelper(serverUrl);
if (credentialHelper != null) {
try {
return credentialHelper.get(serverUrl);
}
catch (IOException ex) {
String message = "Error retrieving credentials for '%s' due to: %s".formatted(serverUrl,
ex.getMessage());
this.credentialHelperExceptionHandler.accept(message, ex);
}
}
return null;
}
private CredentialHelper getCredentialHelper(String serverUrl) {
String name = this.dockerConfig.getCredHelpers().get(serverUrl);
name = (StringUtils.hasText(name)) ? name : this.dockerConfig.getCredsStore();
return (name != null) ? this.credentialHelperFactory.apply(name.trim()) : null;
}
private Entry<String, Auth> getAuthConfigEntry(String serverUrl) {
for (Map.Entry<String, Auth> candidate : this.dockerConfig.getAuths().entrySet()) {
if (candidate.getKey().equals(serverUrl) || candidate.getKey().endsWith("://" + serverUrl)) {
return candidate;
}
}
return null;
}
}

5
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryTokenAuthentication.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2025 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.
@ -19,7 +19,8 @@ package org.springframework.boot.buildpack.platform.docker.configuration; @@ -19,7 +19,8 @@ package org.springframework.boot.buildpack.platform.docker.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Docker registry authentication configuration using a token.
* {@link DockerRegistryAuthentication} for
* {@link DockerRegistryAuthentication#user(String, String, String, String)}.
*
* @author Scott Frederick
*/

5
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryUserAuthentication.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2025 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.
@ -19,7 +19,8 @@ package org.springframework.boot.buildpack.platform.docker.configuration; @@ -19,7 +19,8 @@ package org.springframework.boot.buildpack.platform.docker.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Docker registry authentication configuration using user credentials.
* {@link DockerRegistryAuthentication} for
* {@link DockerRegistryAuthentication#token(String)}.
*
* @author Scott Frederick
*/

22
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 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.
@ -25,7 +25,9 @@ import java.lang.reflect.Method; @@ -25,7 +25,9 @@ import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import com.fasterxml.jackson.databind.JsonNode;
@ -38,6 +40,7 @@ import org.springframework.util.StreamUtils; @@ -38,6 +40,7 @@ import org.springframework.util.StreamUtils;
* Base class for mapped JSON objects.
*
* @author Phillip Webb
* @author Dmytro Nosan
* @since 2.3.0
*/
public class MappedObject {
@ -75,6 +78,23 @@ public class MappedObject { @@ -75,6 +78,23 @@ public class MappedObject {
return valueAt(this, this.node, this.lookup, expression, type);
}
/**
* Get a {@link Map} at the given JSON path expression with a value mapped from a
* related {@link JsonNode}.
* @param <V> the value type
* @param expression the JSON path expression
* @param valueMapper function to map the value from the {@link JsonNode}
* @return the map
* @since 3.5.0
*/
protected <V> Map<String, V> mapAt(String expression, Function<JsonNode, V> valueMapper) {
Map<String, V> map = new LinkedHashMap<>();
getNode().at(expression)
.fields()
.forEachRemaining((entry) -> map.put(entry.getKey(), valueMapper.apply(entry.getValue())));
return Collections.unmodifiableMap(map);
}
/**
* Get children at the given JSON path expression by constructing them using the given
* factory.

98
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java

@ -0,0 +1,98 @@ @@ -0,0 +1,98 @@
/*
* Copyright 2012-2025 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.util.UUID;
import com.sun.jna.Platform;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIOException;
/**
* Tests for {@link CredentialHelper}.
*
* @author Dmytro Nosan
*/
class CredentialHelperTests {
private static CredentialHelper helper;
@BeforeAll
static void setUp() throws Exception {
String executableName = "docker-credential-test" + ((Platform.isWindows()) ? ".bat" : ".sh");
String executable = new ClassPathResource(executableName, CredentialHelperTests.class).getFile()
.getAbsolutePath();
helper = new CredentialHelper(executable);
}
@Test
void getWhenKnowUser() throws Exception {
Credential credentials = helper.get("user.example.com");
assertThat(credentials).isNotNull();
assertThat(credentials.isIdentityToken()).isFalse();
assertThat(credentials.getServerUrl()).isEqualTo("user.example.com");
assertThat(credentials.getUsername()).isEqualTo("username");
assertThat(credentials.getSecret()).isEqualTo("secret");
}
@Test
void getWhenKnowToken() throws Exception {
Credential credentials = helper.get("token.example.com");
assertThat(credentials).isNotNull();
assertThat(credentials.isIdentityToken()).isTrue();
assertThat(credentials.getServerUrl()).isEqualTo("token.example.com");
assertThat(credentials.getUsername()).isEqualTo("<token>");
assertThat(credentials.getSecret()).isEqualTo("secret");
}
@Test
void getWhenCredentialsMissingMessageReturnsNull() throws Exception {
Credential credentials = helper.get("credentials.missing.example.com");
assertThat(credentials).isNull();
}
@Test
void getWhenUsernameMissingMessageReturnsNull() throws Exception {
Credential credentials = helper.get("username.missing.example.com");
assertThat(credentials).isNull();
}
@Test
void getWhenUrlMissingMessageReturnsNull() throws Exception {
Credential credentials = helper.get("url.missing.example.com");
assertThat(credentials).isNull();
}
@Test
void getWhenUnknownErrorThrowsException() {
assertThatIOException().isThrownBy(() -> helper.get("invalid.example.com"))
.withMessageContaining("Unknown error");
}
@Test
void getWhenCommandDoesNotExistErrorThrowsException() {
String name = "docker-credential-%s".formatted(UUID.randomUUID().toString());
assertThatIOException().isThrownBy(() -> new CredentialHelper(name).get("invalid.example.com"))
.withMessageContaining(name);
}
}

90
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialsTests.java

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
/*
* Copyright 2012-2025 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.io.InputStream;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Credential}.
*
* @author Dmytro Nosan
*/
class CredentialsTests {
@Test
@WithResource(name = "credentials.json", content = """
{
"ServerURL": "https://index.docker.io/v1/",
"Username": "user",
"Secret": "secret"
}
""")
void createWhenUserCredentials() throws Exception {
Credential credentials = getCredentials("credentials.json");
assertThat(credentials.getUsername()).isEqualTo("user");
assertThat(credentials.getSecret()).isEqualTo("secret");
assertThat(credentials.getServerUrl()).isEqualTo("https://index.docker.io/v1/");
assertThat(credentials.isIdentityToken()).isFalse();
}
@Test
@WithResource(name = "credentials.json", content = """
{
"ServerURL": "https://index.docker.io/v1/",
"Username": "<token>",
"Secret": "secret"
}
""")
void createWhenTokenCredentials() throws Exception {
Credential credentials = getCredentials("credentials.json");
assertThat(credentials.getUsername()).isEqualTo("<token>");
assertThat(credentials.getSecret()).isEqualTo("secret");
assertThat(credentials.getServerUrl()).isEqualTo("https://index.docker.io/v1/");
assertThat(credentials.isIdentityToken()).isTrue();
}
@Test
@WithResource(name = "credentials.json", content = """
{
"Username": "user",
"Secret": "secret"
}
""")
void createWhenNoServerUrl() throws Exception {
Credential credentials = getCredentials("credentials.json");
assertThat(credentials.getUsername()).isEqualTo("user");
assertThat(credentials.getSecret()).isEqualTo("secret");
assertThat(credentials.getServerUrl()).isNull();
assertThat(credentials.isIdentityToken()).isFalse();
}
private Credential getCredentials(String name) throws IOException {
try (InputStream inputStream = new ClassPathResource(name).getInputStream()) {
return new Credential(SharedObjectMapper.get().readTree(inputStream));
}
}
}

41
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 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.
@ -26,6 +26,7 @@ import java.util.regex.Pattern; @@ -26,6 +26,7 @@ import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.DockerConfig;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfigurationMetadata.DockerContext;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
@ -36,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException @@ -36,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
* Tests for {@link DockerConfigurationMetadata}.
*
* @author Scott Frederick
* @author Dmytro Nosan
*/
class DockerConfigurationMetadataTests extends AbstractJsonTests {
@ -46,6 +48,9 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests { @@ -46,6 +48,9 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests {
this.environment.put("DOCKER_CONFIG", pathToResource("with-context/config.json"));
DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get);
assertThat(config.getConfiguration().getCurrentContext()).isEqualTo("test-context");
assertThat(config.getConfiguration().getAuths()).isEmpty();
assertThat(config.getConfiguration().getCredHelpers()).isEmpty();
assertThat(config.getConfiguration().getCredsStore()).isNull();
assertThat(config.getContext().getDockerHost()).isEqualTo("unix:///home/user/.docker/docker.sock");
assertThat(config.getContext().isTlsVerify()).isFalse();
assertThat(config.getContext().getTlsPath()).isNull();
@ -56,6 +61,9 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests { @@ -56,6 +61,9 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests {
this.environment.put("DOCKER_CONFIG", pathToResource("without-context/config.json"));
DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get);
assertThat(config.getConfiguration().getCurrentContext()).isNull();
assertThat(config.getConfiguration().getAuths()).isEmpty();
assertThat(config.getConfiguration().getCredHelpers()).isEmpty();
assertThat(config.getConfiguration().getCredsStore()).isNull();
assertThat(config.getContext().getDockerHost()).isNull();
assertThat(config.getContext().isTlsVerify()).isFalse();
assertThat(config.getContext().getTlsPath()).isNull();
@ -66,6 +74,9 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests { @@ -66,6 +74,9 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests {
this.environment.put("DOCKER_CONFIG", pathToResource("with-default-context/config.json"));
DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get);
assertThat(config.getConfiguration().getCurrentContext()).isEqualTo("default");
assertThat(config.getConfiguration().getAuths()).isEmpty();
assertThat(config.getConfiguration().getCredHelpers()).isEmpty();
assertThat(config.getConfiguration().getCredsStore()).isNull();
assertThat(config.getContext().getDockerHost()).isNull();
assertThat(config.getContext().isTlsVerify()).isFalse();
assertThat(config.getContext().getTlsPath()).isNull();
@ -95,10 +106,38 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests { @@ -95,10 +106,38 @@ class DockerConfigurationMetadataTests extends AbstractJsonTests {
this.environment.put("DOCKER_CONFIG", "docker-config-dummy-path");
DockerConfigurationMetadata config = DockerConfigurationMetadata.from(this.environment::get);
assertThat(config.getConfiguration().getCurrentContext()).isNull();
assertThat(config.getConfiguration().getAuths()).isEmpty();
assertThat(config.getConfiguration().getCredHelpers()).isEmpty();
assertThat(config.getConfiguration().getCredsStore()).isNull();
assertThat(config.getContext().getDockerHost()).isNull();
assertThat(config.getContext().isTlsVerify()).isFalse();
}
@Test
void configWithAuthIsRead() throws Exception {
this.environment.put("DOCKER_CONFIG", pathToResource("with-auth/config.json"));
DockerConfigurationMetadata metadata = DockerConfigurationMetadata.from(this.environment::get);
DockerConfig configuration = metadata.getConfiguration();
assertThat(configuration.getCredsStore()).isEqualTo("desktop");
assertThat(configuration.getCredHelpers()).hasSize(3)
.containsEntry("azurecr.io", "acr-env")
.containsEntry("ecr.us-east-1.amazonaws.com", "ecr-login")
.containsEntry("gcr.io", "gcr");
assertThat(configuration.getAuths()).hasSize(3).hasEntrySatisfying("https://index.docker.io/v1/", (auth) -> {
assertThat(auth.getUsername()).isEqualTo("username");
assertThat(auth.getPassword()).isEqualTo("password");
assertThat(auth.getEmail()).isEqualTo("test@gmail.com");
}).hasEntrySatisfying("custom-registry.example.com", (auth) -> {
assertThat(auth.getUsername()).isEqualTo("customUser");
assertThat(auth.getPassword()).isEqualTo("customPass");
assertThat(auth.getEmail()).isNull();
}).hasEntrySatisfying("my-registry.example.com", (auth) -> {
assertThat(auth.getUsername()).isEqualTo("user");
assertThat(auth.getPassword()).isEqualTo("password");
assertThat(auth.getEmail()).isNull();
});
}
private String pathToResource(String resource) throws URISyntaxException {
URL url = getClass().getResource(resource);
return Paths.get(url.toURI()).getParent().toAbsolutePath().toString();

357
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryConfigAuthenticationTests.java

@ -0,0 +1,357 @@ @@ -0,0 +1,357 @@
/*
* Copyright 2012-2025 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.io.InputStream;
import java.nio.file.Path;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
import org.springframework.boot.testsupport.classpath.resources.ResourcesRoot;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.boot.testsupport.system.OutputCaptureExtension;
import org.springframework.core.io.ClassPathResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DockerRegistryConfigAuthentication}.
*
* @author Dmytro Nosan
* @author Phillip Webb
*/
@ExtendWith(OutputCaptureExtension.class)
class DockerRegistryConfigAuthenticationTests {
private final Map<String, String> environment = new LinkedHashMap<>();
private final Map<String, Exception> helperExceptions = new LinkedHashMap<>();
private Map<String, CredentialHelper> credentialHelpers = new HashMap<>();
@WithResource(name = "config.json", content = """
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=",
"email": "test@gmail.com"
}
}
}
""")
@Test
void getAuthHeaderWhenAuthForDockerDomain(@ResourcesRoot Path directory) throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("docker.io/ubuntu:latest");
String authHeader = getAuthHeader(imageReference);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "https://index.docker.io/v1/")
.containsEntry("username", "username")
.containsEntry("password", "password")
.containsEntry("email", "test@gmail.com");
}
@WithResource(name = "config.json", content = """
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=",
"email": "test@gmail.com"
}
}
}
""")
@Test
void getAuthHeaderWhenAuthForLegacyDockerDomain(@ResourcesRoot Path directory) throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("index.docker.io/ubuntu:latest");
String authHeader = getAuthHeader(imageReference);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "https://index.docker.io/v1/")
.containsEntry("username", "username")
.containsEntry("password", "password")
.containsEntry("email", "test@gmail.com");
}
@WithResource(name = "config.json", content = """
{
"auths": {
"my-registry.example.com": {
"auth": "Y3VzdG9tVXNlcjpjdXN0b21QYXNz"
}
}
}
""")
@Test
void getAuthHeaderWhenAuthForCustomDomain(@ResourcesRoot Path directory) throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("my-registry.example.com/ubuntu:latest");
String authHeader = getAuthHeader(imageReference);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "my-registry.example.com")
.containsEntry("username", "customUser")
.containsEntry("password", "customPass")
.containsEntry("email", null);
}
@WithResource(name = "config.json", content = """
{
"auths": {
"https://my-registry.example.com": {
"auth": "Y3VzdG9tVXNlcjpjdXN0b21QYXNz"
}
}
}
""")
@Test
void getAuthHeaderWhenAuthForCustomDomainWithLegacyFormat(@ResourcesRoot Path directory) throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("my-registry.example.com/ubuntu:latest");
String authHeader = getAuthHeader(imageReference);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "https://my-registry.example.com")
.containsEntry("username", "customUser")
.containsEntry("password", "customPass")
.containsEntry("email", null);
}
@WithResource(name = "config.json", content = """
{
}
""")
@Test
void getAuthHeaderWhenEmptyConfigDirectoryReturnsFallback(@ResourcesRoot Path directory) throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("docker.io/ubuntu:latest");
String authHeader = getAuthHeader(imageReference, DockerRegistryAuthentication.EMPTY_USER);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "")
.containsEntry("username", "")
.containsEntry("password", "")
.containsEntry("email", "");
}
@WithResource(name = "config.json", content = """
{
"credsStore": "desktop"
}
""")
@WithResource(name = "credentials.json", content = """
{
"ServerURL": "https://index.docker.io/v1/",
"Username": "<token>",
"Secret": "secret"
}
""")
@Test
void getAuthHeaderWhenUsingHelperFromCredsStore(@ResourcesRoot Path directory) throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("docker.io/ubuntu:latest");
mockHelper("desktop", "https://index.docker.io/v1/", "credentials.json");
String authHeader = getAuthHeader(imageReference);
assertThat(decode(authHeader)).hasSize(1).containsEntry("identitytoken", "secret");
}
@WithResource(name = "config.json", content = """
{
"auths": {
"gcr.io": {
"email": "test@gmail.com"
}
},
"credsStore": "desktop",
"credHelpers": {
"gcr.io": "gcr"
}
}
""")
@WithResource(name = "credentials.json", content = """
{
"ServerURL": "https://my-gcr.io",
"Username": "username",
"Secret": "secret"
}
""")
@Test
void getAuthHeaderWhenUsingHelperFromCredsStoreAndUseEmailFromAuth(@ResourcesRoot Path directory) throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
mockHelper("gcr", "gcr.io", "credentials.json");
String authHeader = getAuthHeader(imageReference);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "https://my-gcr.io")
.containsEntry("username", "username")
.containsEntry("password", "secret")
.containsEntry("email", "test@gmail.com");
}
@WithResource(name = "config.json", content = """
{
"credsStore": "desktop",
"credHelpers": {
"gcr.io": "gcr"
}
}
""")
@WithResource(name = "credentials.json", content = """
{
"Username": "username",
"Secret": "secret"
}
""")
@Test
void getAuthHeaderWhenUsingHelperFromCredHelpersUsesProvidedServerUrl(@ResourcesRoot Path directory)
throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
mockHelper("gcr", "gcr.io", "credentials.json");
String authHeader = getAuthHeader(imageReference);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "gcr.io")
.containsEntry("username", "username")
.containsEntry("password", "secret")
.containsEntry("email", null);
}
@WithResource(name = "config.json", content = """
{
"auths": {
"gcr.io": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=",
"email": "test@gmail.com"
}
},
"credsStore": "desktop",
"credHelpers": {
"gcr.io": "gcr"
}
}
""")
@Test
void getAuthHeaderWhenUsingHelperThatFailsLogsErrorAndReturnsFromAuths(@ResourcesRoot Path directory)
throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
CredentialHelper helper = mockHelper("gcr");
given(helper.get("gcr.io")).willThrow(new IOException("Failed to obtain credentials for registry"));
String authHeader = getAuthHeader(imageReference);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "gcr.io")
.containsEntry("username", "username")
.containsEntry("password", "password")
.containsEntry("email", "test@gmail.com");
assertThat(this.helperExceptions).hasSize(1);
assertThat(this.helperExceptions.keySet().iterator().next())
.contains("Error retrieving credentials for 'gcr.io' due to: Failed to obtain credentials for registry");
}
@WithResource(name = "config.json", content = """
{
"credsStore": "desktop",
"credHelpers": {
"gcr.io": "gcr"
}
}
""")
@Test
void getAuthHeaderWhenUsingHelperThatFailsAndNoAuthLogsErrorAndReturnsFallback(@ResourcesRoot Path directory)
throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
CredentialHelper helper = mockHelper("gcr");
given(helper.get("gcr.io")).willThrow(new IOException("Failed to obtain credentials for registry"));
String authHeader = getAuthHeader(imageReference, DockerRegistryAuthentication.EMPTY_USER);
assertThat(this.helperExceptions).hasSize(1);
assertThat(this.helperExceptions.keySet().iterator().next())
.contains("Error retrieving credentials for 'gcr.io' due to: Failed to obtain credentials for registry");
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "")
.containsEntry("username", "")
.containsEntry("password", "")
.containsEntry("email", "");
}
@WithResource(name = "config.json", content = """
{
"credsStore": "desktop",
"credHelpers": {
"gcr.io": ""
}
}
""")
@Test
void getAuthHeaderWhenEmptyCredHelperReturnsFallbackAndDoesNotUseCredStore(@ResourcesRoot Path directory)
throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
String authHeader = getAuthHeader(imageReference, DockerRegistryAuthentication.EMPTY_USER);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "")
.containsEntry("username", "")
.containsEntry("password", "")
.containsEntry("email", "");
}
private String getAuthHeader(ImageReference imageReference) {
return getAuthHeader(imageReference, null);
}
private String getAuthHeader(ImageReference imageReference, DockerRegistryAuthentication fallback) {
DockerRegistryConfigAuthentication authentication = getAuthentication(fallback);
return authentication.getAuthHeader(imageReference);
}
private DockerRegistryConfigAuthentication getAuthentication(DockerRegistryAuthentication fallback) {
return new DockerRegistryConfigAuthentication(fallback, this.helperExceptions::put, this.environment::get,
this.credentialHelpers::get);
}
private void mockHelper(String name, String serverUrl, String credentialsResourceName) throws Exception {
CredentialHelper helper = mockHelper(name);
given(helper.get(serverUrl)).willReturn(getCredentials(credentialsResourceName));
}
private CredentialHelper mockHelper(String name) {
CredentialHelper helper = mock(CredentialHelper.class);
this.credentialHelpers.put(name, helper);
return helper;
}
private Credential getCredentials(String resourceName) throws Exception {
try (InputStream inputStream = new ClassPathResource(resourceName).getInputStream()) {
return new Credential(SharedObjectMapper.get().readTree(inputStream));
}
}
private Map<String, String> decode(String authHeader) throws Exception {
assertThat(authHeader).isNotNull();
return SharedObjectMapper.get().readValue(Base64.getDecoder().decode(authHeader), new TypeReference<>() {
});
}
}

39
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/docker-credential-test.bat

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
@echo off
set /p registryUrl=
if "%registryUrl%" == "user.example.com" (
echo {
echo "ServerURL": "%registryUrl%",
echo "Username": "username",
echo "Secret": "secret"
echo }
exit /b 0
)
if "%registryUrl%" == "token.example.com" (
echo {
echo "ServerURL": "%registryUrl%",
echo "Username": "<token>",
echo "Secret": "secret"
echo }
exit /b 0
)
if "%registryUrl%" == "url.missing.example.com" (
echo no credentials server URL >&2
exit /b 1
)
if "%registryUrl%" == "username.missing.example.com" (
echo no credentials username >&2
exit /b 1
)
if "%registryUrl%" == "credentials.missing.example.com" (
echo credentials not found in native keychain >&2
exit /b 1
)
echo Unknown error >&2
exit /b 1

43
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/docker-credential-test.sh

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
#!/bin/sh
read -r registryUrl
if [ "$registryUrl" = "user.example.com" ]; then
cat <<EOF
{
"ServerURL": "${registryUrl}",
"Username": "username",
"Secret": "secret"
}
EOF
exit 0
fi
if [ "$registryUrl" = "token.example.com" ]; then
cat <<EOF
{
"ServerURL": "${registryUrl}",
"Username": "<token>",
"Secret": "secret"
}
EOF
exit 0
fi
if [ "$registryUrl" = "url.missing.example.com" ]; then
echo "no credentials server URL" >&2
exit 1
fi
if [ "$registryUrl" = "username.missing.example.com" ]; then
echo "no credentials username" >&2
exit 1
fi
if [ "$registryUrl" = "credentials.missing.example.com" ]; then
echo "credentials not found in native keychain" >&2
exit 1
fi
echo "Unknown error" >&2
exit 1

21
spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-auth/config.json

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=",
"email": "test@gmail.com"
},
"custom-registry.example.com": {
"auth": "Y3VzdG9tVXNlcjpjdXN0b21QYXNz"
},
"my-registry.example.com": {
"username": "user",
"password": "password"
}
},
"credsStore": "desktop",
"credHelpers": {
"gcr.io": "gcr",
"ecr.us-east-1.amazonaws.com": "ecr-login",
"azurecr.io": "acr-env"
}
}

12
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc

@ -100,6 +100,18 @@ The following table summarizes the available properties for `docker.builderRegis @@ -100,6 +100,18 @@ The following table summarizes the available properties for `docker.builderRegis
For more details, see also xref:packaging-oci-image.adoc#build-image.examples.docker[examples].
[NOTE]
====
If credentials are not provided, the plugin reads the user's existing Docker configuration file (typically located at `$HOME/.docker/config.json`) to determine authentication methods.
Using these methods, the plugin attempts to provide authentication credentials for the requested image.
The plugin supports the following authentication methods:
- *Credential Helpers*: External tools configured in the Docker configuration file to provide credentials for specific registries. For example, tools like `osxkeychain` or `ecr-login` handle authentication for certain registries.
- *Credential Store*: A default fallback mechanism that securely stores and retrieves credentials (e.g., `desktop` for Docker Desktop).
- *Static Credentials*: Credentials that are stored directly in the Docker configuration file under the `auths` section.
====
[[build-image.customization]]

9
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java

@ -145,13 +145,14 @@ public abstract class DockerSpec { @@ -145,13 +145,14 @@ public abstract class DockerSpec {
}
private BuilderDockerConfiguration customizeBuilderAuthentication(BuilderDockerConfiguration dockerConfiguration) {
return dockerConfiguration
.withBuilderRegistryAuthentication(getRegistryAuthentication("builder", this.builderRegistry, null));
return dockerConfiguration.withBuilderRegistryAuthentication(getRegistryAuthentication("builder",
this.builderRegistry, DockerRegistryAuthentication.configuration(null)));
}
private BuilderDockerConfiguration customizePublishAuthentication(BuilderDockerConfiguration dockerConfiguration) {
return dockerConfiguration.withPublishRegistryAuthentication(
getRegistryAuthentication("publish", this.publishRegistry, DockerRegistryAuthentication.EMPTY_USER));
return dockerConfiguration
.withPublishRegistryAuthentication(getRegistryAuthentication("publish", this.publishRegistry,
DockerRegistryAuthentication.configuration(DockerRegistryAuthentication.EMPTY_USER)));
}
private DockerRegistryAuthentication getRegistryAuthentication(String type, DockerRegistrySpec registry,

12
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java

@ -54,7 +54,7 @@ class DockerSpecTests { @@ -54,7 +54,7 @@ class DockerSpecTests {
void asDockerConfigurationWithDefaults() {
BuilderDockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration();
assertThat(dockerConfiguration.connection()).isNull();
assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull();
assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull();
assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
@ -73,7 +73,7 @@ class DockerSpecTests { @@ -73,7 +73,7 @@ class DockerSpecTests {
assertThat(host.secure()).isTrue();
assertThat(host.certificatePath()).isEqualTo("/tmp/ca-cert");
assertThat(dockerConfiguration.bindHostToBuilder()).isFalse();
assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull();
assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull();
assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
@ -90,7 +90,7 @@ class DockerSpecTests { @@ -90,7 +90,7 @@ class DockerSpecTests {
assertThat(host.secure()).isFalse();
assertThat(host.certificatePath()).isNull();
assertThat(dockerConfiguration.bindHostToBuilder()).isFalse();
assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull();
assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull();
assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
@ -106,7 +106,7 @@ class DockerSpecTests { @@ -106,7 +106,7 @@ class DockerSpecTests {
.connection();
assertThat(host.context()).isEqualTo("test-context");
assertThat(dockerConfiguration.bindHostToBuilder()).isFalse();
assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull();
assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull();
assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
@ -132,7 +132,7 @@ class DockerSpecTests { @@ -132,7 +132,7 @@ class DockerSpecTests {
assertThat(host.secure()).isFalse();
assertThat(host.certificatePath()).isNull();
assertThat(dockerConfiguration.bindHostToBuilder()).isTrue();
assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull();
assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull();
assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
@ -213,7 +213,7 @@ class DockerSpecTests { @@ -213,7 +213,7 @@ class DockerSpecTests {
}
String decoded(String value) {
return new String(Base64.getDecoder().decode(value));
return (value != null) ? new String(Base64.getDecoder().decode(value)) : value;
}
}

12
spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc

@ -115,6 +115,18 @@ The following table summarizes the available parameters for `docker.builderRegis @@ -115,6 +115,18 @@ The following table summarizes the available parameters for `docker.builderRegis
For more details, see also xref:build-image.adoc#build-image.examples.docker[examples].
[NOTE]
====
If credentials are not provided, the plugin reads the user's existing Docker configuration file (typically located at `$HOME/.docker/config.json`) to determine authentication methods.
Using these methods, the plugin attempts to provide authentication credentials for the requested image.
The plugin supports the following authentication methods:
- *Credential Helpers*: External tools configured in the Docker configuration file to provide credentials for specific registries. For example, tools like `osxkeychain` or `ecr-login` handle authentication for certain registries.
- *Credential Store*: A default fallback mechanism that securely stores and retrieves credentials (e.g., `desktop` for Docker Desktop).
- *Static Credentials*: Credentials that are stored directly in the Docker configuration file under the `auths` section.
====
[[build-image.customization]]

6
spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java

@ -262,9 +262,9 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo { @@ -262,9 +262,9 @@ public abstract class BuildImageMojo extends AbstractPackagerMojo {
Libraries libraries = getLibraries(Collections.emptySet());
try {
BuildRequest request = getBuildRequest(libraries);
BuilderDockerConfiguration dockerConfiguration = (this.docker != null)
? this.docker.asDockerConfiguration(request.isPublish())
: new Docker().asDockerConfiguration(request.isPublish());
Docker docker = (this.docker != null) ? this.docker : new Docker();
BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(getLog(),
request.isPublish());
Builder builder = new Builder(new MojoBuildLog(this::getLog), dockerConfiguration);
builder.build(request);
}

26
spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
package org.springframework.boot.maven;
import org.apache.maven.plugin.logging.Log;
import org.springframework.boot.buildpack.platform.build.BuilderDockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
@ -141,15 +143,16 @@ public class Docker { @@ -141,15 +143,16 @@ public class Docker {
* Returns this configuration as a {@link BuilderDockerConfiguration} instance. This
* method should only be called when the configuration is complete and will no longer
* be changed.
* @param log the output log
* @param publish whether the image should be published
* @return the Docker configuration
*/
BuilderDockerConfiguration asDockerConfiguration(boolean publish) {
BuilderDockerConfiguration asDockerConfiguration(Log log, boolean publish) {
BuilderDockerConfiguration dockerConfiguration = new BuilderDockerConfiguration();
dockerConfiguration = customizeHost(dockerConfiguration);
dockerConfiguration = dockerConfiguration.withBindHostToBuilder(this.bindHostToBuilder);
dockerConfiguration = customizeBuilderAuthentication(dockerConfiguration);
dockerConfiguration = customizePublishAuthentication(dockerConfiguration, publish);
dockerConfiguration = customizeBuilderAuthentication(log, dockerConfiguration);
dockerConfiguration = customizePublishAuthentication(log, dockerConfiguration, publish);
return dockerConfiguration;
}
@ -167,18 +170,23 @@ public class Docker { @@ -167,18 +170,23 @@ public class Docker {
return dockerConfiguration;
}
private BuilderDockerConfiguration customizeBuilderAuthentication(BuilderDockerConfiguration dockerConfiguration) {
return dockerConfiguration
.withBuilderRegistryAuthentication(getRegistryAuthentication("builder", this.builderRegistry, null));
private BuilderDockerConfiguration customizeBuilderAuthentication(Log log,
BuilderDockerConfiguration dockerConfiguration) {
DockerRegistryAuthentication authentication = DockerRegistryAuthentication.configuration(null,
(message, ex) -> log.warn(message));
return dockerConfiguration.withBuilderRegistryAuthentication(
getRegistryAuthentication("builder", this.builderRegistry, authentication));
}
private BuilderDockerConfiguration customizePublishAuthentication(BuilderDockerConfiguration dockerConfiguration,
boolean publish) {
private BuilderDockerConfiguration customizePublishAuthentication(Log log,
BuilderDockerConfiguration dockerConfiguration, boolean publish) {
if (!publish) {
return dockerConfiguration;
}
DockerRegistryAuthentication authentication = DockerRegistryAuthentication
.configuration(DockerRegistryAuthentication.EMPTY_USER, (message, ex) -> log.warn(message));
return dockerConfiguration.withPublishRegistryAuthentication(
getRegistryAuthentication("publish", this.publishRegistry, DockerRegistryAuthentication.EMPTY_USER));
getRegistryAuthentication("publish", this.publishRegistry, authentication));
}
private DockerRegistryAuthentication getRegistryAuthentication(String type, DockerRegistry registry,

18
spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java

@ -18,6 +18,8 @@ package org.springframework.boot.maven; @@ -18,6 +18,8 @@ package org.springframework.boot.maven;
import java.util.Base64;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.logging.SystemStreamLog;
import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.build.BuilderDockerConfiguration;
@ -34,12 +36,14 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException @@ -34,12 +36,14 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*/
class DockerTests {
private final Log log = new SystemStreamLog();
@Test
void asDockerConfigurationWithDefaults() {
Docker docker = new Docker();
BuilderDockerConfiguration dockerConfiguration = createDockerConfiguration(docker);
assertThat(dockerConfiguration.connection()).isNull();
assertThat(dockerConfiguration.builderRegistryAuthentication()).isNull();
assertThat(dockerConfiguration.builderRegistryAuthentication().getAuthHeader()).isNull();
assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
@ -59,7 +63,7 @@ class DockerTests { @@ -59,7 +63,7 @@ class DockerTests {
assertThat(host.secure()).isTrue();
assertThat(host.certificatePath()).isEqualTo("/tmp/ca-cert");
assertThat(dockerConfiguration.bindHostToBuilder()).isFalse();
assertThat(createDockerConfiguration(docker).builderRegistryAuthentication()).isNull();
assertThat(createDockerConfiguration(docker).builderRegistryAuthentication().getAuthHeader()).isNull();
assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
@ -76,7 +80,7 @@ class DockerTests { @@ -76,7 +80,7 @@ class DockerTests {
.connection();
assertThat(context.context()).isEqualTo("test-context");
assertThat(dockerConfiguration.bindHostToBuilder()).isFalse();
assertThat(createDockerConfiguration(docker).builderRegistryAuthentication()).isNull();
assertThat(createDockerConfiguration(docker).builderRegistryAuthentication().getAuthHeader()).isNull();
assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
@ -106,7 +110,7 @@ class DockerTests { @@ -106,7 +110,7 @@ class DockerTests {
assertThat(host.secure()).isTrue();
assertThat(host.certificatePath()).isEqualTo("/tmp/ca-cert");
assertThat(dockerConfiguration.bindHostToBuilder()).isTrue();
assertThat(createDockerConfiguration(docker).builderRegistryAuthentication()).isNull();
assertThat(createDockerConfiguration(docker).builderRegistryAuthentication().getAuthHeader()).isNull();
assertThat(decoded(dockerConfiguration.publishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
@ -157,7 +161,7 @@ class DockerTests { @@ -157,7 +161,7 @@ class DockerTests {
Docker docker = new Docker();
docker.setPublishRegistry(
new Docker.DockerRegistry("user", null, "https://docker.example.com", "docker@example.com"));
BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(false);
BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(this.log, false);
assertThat(dockerConfiguration.publishRegistryAuthentication()).isNull();
}
@ -193,12 +197,12 @@ class DockerTests { @@ -193,12 +197,12 @@ class DockerTests {
dockerRegistry.setToken("token");
Docker docker = new Docker();
docker.setPublishRegistry(dockerRegistry);
BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(false);
BuilderDockerConfiguration dockerConfiguration = docker.asDockerConfiguration(this.log, false);
assertThat(dockerConfiguration.publishRegistryAuthentication()).isNull();
}
private BuilderDockerConfiguration createDockerConfiguration(Docker docker) {
return docker.asDockerConfiguration(true);
return docker.asDockerConfiguration(this.log, true);
}
String decoded(String value) {

Loading…
Cancel
Save