From e6fbccef7e7d58162fb8c064cf7b8274a9d3ec83 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Thu, 24 Apr 2025 11:57:53 -0700 Subject: [PATCH] Support Docker configuration authentication including helper support Add `DockerRegistryAuthentication` implementation that uses standard Docker config to authenticate requests. Prior to this commit, we only supported username/password and token based authentication. This commit allows authentication based on the contents of the Docker configuration file, including support for executing credential helpers. See gh-45269 Signed-off-by: Dmytro Nosan Co-authored-by: Phillip Webb --- .../buildpack/platform/build/Builder.java | 10 +- .../docker/configuration/Credential.java | 68 ++++ .../configuration/CredentialHelper.java | 103 +++++ .../DockerConfigurationMetadata.java | 21 +- .../DockerRegistryAuthentication.java | 45 +++ .../DockerRegistryConfigAuthentication.java | 150 ++++++++ .../DockerRegistryTokenAuthentication.java | 5 +- .../DockerRegistryUserAuthentication.java | 5 +- .../configuration/CredentialHelperTests.java | 98 +++++ .../configuration/CredentialsTests.java | 90 +++++ ...ckerRegistryConfigAuthenticationTests.java | 357 ++++++++++++++++++ .../configuration/docker-credential-test.bat | 39 ++ .../configuration/docker-credential-test.sh | 43 +++ 13 files changed, 1022 insertions(+), 12 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/Credential.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryConfigAuthentication.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialsTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/DockerRegistryConfigAuthenticationTests.java create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/docker-credential-test.bat create mode 100755 spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/configuration/docker-credential-test.sh 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 0bf2e33213..2544de9ec8 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 0000000000..c217e20dd5 --- /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 0000000000..800ddf4e3f --- /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 841b2ddfdc..891cbf5fba 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 @@ -63,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; @@ -88,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) { 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 1e4f4a1dbf..1714f1cbe4 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 0000000000..f14c072580 --- /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 d0923c22cd..c3c13c81d1 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 c5a068e730..b887c8db2a 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/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 0000000000..480a46a389 --- /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 0000000000..903f16326d --- /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/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 0000000000..7d551bad02 --- /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 0000000000..ce47ef659d --- /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 0000000000..d69879398c --- /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