diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java index 0bf2e33213d..2544de9ec8d 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Builder.java +++ b/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 { private void pushImage(ImageReference reference) throws IOException { Consumer 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 { 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 { 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) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/Credential.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/Credential.java new file mode 100644 index 00000000000..c217e20dd57 --- /dev/null +++ b/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 @@ +/* + * 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 }. + */ + private static final String TOKEN_USERNAME = ""; + + 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); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java new file mode 100644 index 00000000000..800ddf4e3fd --- /dev/null +++ b/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 @@ +/* + * 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 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 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()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java index 9ab0fef2019..891cbf5fbaa 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadata.java +++ b/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 @@ /* - * 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; 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; 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 { 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 { } 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 { private final String currentContext; + private final String credsStore; + + private final Map credHelpers; + + private final Map 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 getCredHelpers() { + return this.credHelpers; + } + + Map 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 { } + 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; diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java index 1e4f4a1dbf5..1714f1cbe44 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryAuthentication.java +++ b/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 @@ 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 { */ 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 { 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 credentialHelperExceptionHandler) { + return new DockerRegistryConfigAuthentication(fallback, credentialHelperExceptionHandler); + } + } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryConfigAuthentication.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryConfigAuthentication.java new file mode 100644 index 00000000000..f14c0725802 --- /dev/null +++ b/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 @@ +/* + * 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 credentialFromHelperCache = new ConcurrentHashMap<>(); + + private final DockerRegistryAuthentication fallback; + + private final BiConsumer credentialHelperExceptionHandler; + + private final Function credentialHelperFactory; + + private final DockerConfig dockerConfig; + + DockerRegistryConfigAuthentication(DockerRegistryAuthentication fallback, + BiConsumer credentialHelperExceptionHandler) { + this(fallback, credentialHelperExceptionHandler, Environment.SYSTEM, + (helper) -> new CredentialHelper("docker-credential-" + helper.trim())); + } + + DockerRegistryConfigAuthentication(DockerRegistryAuthentication fallback, + BiConsumer credentialHelperExceptionHandler, Environment environment, + Function 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 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 getAuthConfigEntry(String serverUrl) { + for (Map.Entry candidate : this.dockerConfig.getAuths().entrySet()) { + if (candidate.getKey().equals(serverUrl) || candidate.getKey().endsWith("://" + serverUrl)) { + return candidate; + } + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryTokenAuthentication.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryTokenAuthentication.java index d0923c22cd1..c3c13c81d13 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryTokenAuthentication.java +++ b/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 @@ /* - * 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; 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 */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryUserAuthentication.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryUserAuthentication.java index c5a068e7301..b887c8db2ab 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryUserAuthentication.java +++ b/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 @@ /* - * 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; import com.fasterxml.jackson.annotation.JsonProperty; /** - * Docker registry authentication configuration using user credentials. + * {@link DockerRegistryAuthentication} for + * {@link DockerRegistryAuthentication#token(String)}. * * @author Scott Frederick */ diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java index 08a16a206a3..936e57bfe8f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/json/MappedObject.java +++ b/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 @@ /* - * 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; 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; * 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 { 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 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 Map mapAt(String expression, Function valueMapper) { + Map 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. diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java new file mode 100644 index 00000000000..480a46a3890 --- /dev/null +++ b/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 @@ +/* + * 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(""); + 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); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialsTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialsTests.java new file mode 100644 index 00000000000..903f16326dc --- /dev/null +++ b/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 @@ +/* + * 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": "", + "Secret": "secret" + } + """) + void createWhenTokenCredentials() throws Exception { + Credential credentials = getCredentials("credentials.json"); + assertThat(credentials.getUsername()).isEqualTo(""); + 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)); + } + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java index b47bbaa3d80..4e90ab85309 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerConfigurationMetadataTests.java +++ b/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 @@ /* - * 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; 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 * Tests for {@link DockerConfigurationMetadata}. * * @author Scott Frederick + * @author Dmytro Nosan */ 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 { 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 { 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 { 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(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryConfigAuthenticationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryConfigAuthenticationTests.java new file mode 100644 index 00000000000..7d551bad022 --- /dev/null +++ b/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 @@ +/* + * 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 environment = new LinkedHashMap<>(); + + private final Map helperExceptions = new LinkedHashMap<>(); + + private Map 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": "", + "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 decode(String authHeader) throws Exception { + assertThat(authHeader).isNotNull(); + return SharedObjectMapper.get().readValue(Base64.getDecoder().decode(authHeader), new TypeReference<>() { + }); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/docker-credential-test.bat b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/docker-credential-test.bat new file mode 100644 index 00000000000..ce47ef659d5 --- /dev/null +++ b/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 @@ +@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": "", + 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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/docker-credential-test.sh b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/docker-credential-test.sh new file mode 100755 index 00000000000..d69879398c1 --- /dev/null +++ b/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 @@ +#!/bin/sh + +read -r registryUrl + +if [ "$registryUrl" = "user.example.com" ]; then + cat <", + "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 diff --git a/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-auth/config.json b/spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/with-auth/config.json new file mode 100644 index 00000000000..5ccd4b0bf21 --- /dev/null +++ b/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 @@ +{ + "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" + } +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc index 92dfb77043f..5f408d440ab 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc +++ b/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 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]] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java index 62843a38152..c69e85ccc29 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/DockerSpec.java +++ b/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 { } 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, diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java index f968dee3f6f..7c71cf1c727 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/DockerSpecTests.java +++ b/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 { 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 { 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 { 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 { .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 { 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 { } String decoded(String value) { - return new String(Base64.getDecoder().decode(value)); + return (value != null) ? new String(Base64.getDecoder().decode(value)) : value; } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc index 7e37f8d36d5..f4e2642f931 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc +++ b/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 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]] diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java index 74f8aaec2c7..503b23e7472 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/BuildImageMojo.java +++ b/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 { 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); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java index 1f8f7c0708c..92535afef95 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Docker.java @@ -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 { * 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 { 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, diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java index 50342a7e567..770eb98edf3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/DockerTests.java +++ b/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; 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 */ 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 { 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 { .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 { 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 { 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 { 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) {