This commit is contained in:
Dmytro Nosan 2025-04-24 04:55:47 +00:00 committed by GitHub
commit 5bd1c6678b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1324 additions and 149 deletions

View File

@ -27,6 +27,7 @@ import org.springframework.boot.buildpack.platform.docker.TotalProgressPullListe
import org.springframework.boot.buildpack.platform.docker.TotalProgressPushListener;
import org.springframework.boot.buildpack.platform.docker.UpdateListener;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerRegistryAuthentication;
import org.springframework.boot.buildpack.platform.docker.configuration.ResolvedDockerHost;
import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException;
import org.springframework.boot.buildpack.platform.docker.type.Binding;
@ -102,9 +103,8 @@ public class Builder {
Assert.notNull(request, "'request' must not be null");
this.log.start(request);
validateBindings(request.getBindings());
String domain = request.getBuilder().getDomain();
PullPolicy pullPolicy = request.getPullPolicy();
ImageFetcher imageFetcher = new ImageFetcher(domain, getBuilderAuthHeader(), pullPolicy,
ImageFetcher imageFetcher = new ImageFetcher(getBuilderRegistryAuthentication(), pullPolicy,
request.getImagePlatform());
Image builderImage = imageFetcher.fetchImage(ImageType.BUILDER, request.getBuilder());
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
@ -203,18 +203,20 @@ public class Builder {
private void pushImage(ImageReference reference) throws IOException {
Consumer<TotalProgressEvent> progressConsumer = this.log.pushingImage(reference);
TotalProgressPushListener listener = new TotalProgressPushListener(progressConsumer);
this.docker.image().push(reference, listener, getPublishAuthHeader());
this.docker.image().push(reference, listener, getPublishAuthHeader(reference));
this.log.pushedImage(reference);
}
private String getBuilderAuthHeader() {
return (this.dockerConfiguration != null && this.dockerConfiguration.getBuilderRegistryAuthentication() != null)
? this.dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader() : null;
private DockerRegistryAuthentication getBuilderRegistryAuthentication() {
if (this.dockerConfiguration != null) {
return this.dockerConfiguration.getBuilderRegistryAuthentication();
}
return null;
}
private String getPublishAuthHeader() {
private String getPublishAuthHeader(ImageReference imageReference) {
return (this.dockerConfiguration != null && this.dockerConfiguration.getPublishRegistryAuthentication() != null)
? this.dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader() : null;
? this.dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader(imageReference) : null;
}
/**
@ -222,17 +224,14 @@ public class Builder {
*/
private class ImageFetcher {
private final String domain;
private final String authHeader;
private final DockerRegistryAuthentication authentication;
private final PullPolicy pullPolicy;
private ImagePlatform defaultPlatform;
ImageFetcher(String domain, String authHeader, PullPolicy pullPolicy, ImagePlatform platform) {
this.domain = domain;
this.authHeader = authHeader;
ImageFetcher(DockerRegistryAuthentication authentication, PullPolicy pullPolicy, ImagePlatform platform) {
this.authentication = authentication;
this.pullPolicy = pullPolicy;
this.defaultPlatform = platform;
}
@ -240,27 +239,25 @@ 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");
Assert.state(this.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));
String authHeader = getAuthHeader(reference);
if (this.pullPolicy == PullPolicy.ALWAYS) {
return checkPlatformMismatch(pullImage(reference, type), reference);
return checkPlatformMismatch(pullImage(authHeader, reference, type), reference);
}
try {
return checkPlatformMismatch(Builder.this.docker.image().inspect(reference), reference);
}
catch (DockerEngineException ex) {
if (this.pullPolicy == PullPolicy.IF_NOT_PRESENT && ex.getStatusCode() == 404) {
return checkPlatformMismatch(pullImage(reference, type), reference);
return checkPlatformMismatch(pullImage(authHeader, reference, type), reference);
}
throw ex;
}
}
private Image pullImage(ImageReference reference, ImageType imageType) throws IOException {
private Image pullImage(String authHeader, ImageReference reference, ImageType imageType) throws IOException {
TotalProgressPullListener listener = new TotalProgressPullListener(
Builder.this.log.pullingImage(reference, this.defaultPlatform, imageType));
Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, this.authHeader);
Image image = Builder.this.docker.image().pull(reference, this.defaultPlatform, listener, authHeader);
Builder.this.log.pulledImage(image, imageType);
if (this.defaultPlatform == null) {
this.defaultPlatform = ImagePlatform.from(image);
@ -278,6 +275,10 @@ public class Builder {
return image;
}
private String getAuthHeader(ImageReference reference) {
return (this.authentication != null) ? this.authentication.getAuthHeader(reference) : null;
}
}
private static final class PlatformMismatchException extends RuntimeException {

View File

@ -0,0 +1,87 @@
/*
* 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.
*
* @author Dmytro Nosan
*/
class Credentials extends MappedObject {
/**
* If the secret being stored is an identity token, the username should be set to
* {@code <token>}.
*/
private static final String TOKEN_USERNAME = "<token>";
private final String serverUrl;
private final String username;
private final String secret;
/**
* Create a new {@link Credentials} instance from the given JSON node.
* @param node the JSON node to read from
*/
Credentials(JsonNode node) {
super(node, MethodHandles.lookup());
this.serverUrl = valueAt("/ServerURL", String.class);
this.username = valueAt("/Username", String.class);
this.secret = valueAt("/Secret", String.class);
}
/**
* Checks if the secret being stored is an identity token.
* @return true if the secret is an identity token, false otherwise
*/
boolean isIdentityToken() {
return TOKEN_USERNAME.equals(this.username);
}
/**
* Returns the server URL associated with this credential.
* @return the server URL
*/
String getServerUrl() {
return this.serverUrl;
}
/**
* Returns the username associated with the credential.
* @return the username
*/
String getUsername() {
return this.username;
}
/**
* Returns the secret associated with this credential.
* @return the secret
*/
String getSecret() {
return this.secret;
}
}

View File

@ -0,0 +1,112 @@
/*
* 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 com.sun.jna.Platform;
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
/**
* Default implementation of the {@link DockerCredentialHelper} that retrieves Docker
* credentials using a specified credential helper.
*
* @author Dmytro Nosan
*/
class DefaultDockerCredentialHelper implements DockerCredentialHelper {
private static final String USR_LOCAL_BIN = "/usr/local/bin/";
private static final String CREDENTIALS_NOT_FOUND = "credentials not found in native keychain";
private static final String CREDENTIALS_URL_MISSING = "no credentials server URL";
private static final String CREDENTIALS_USERNAME_MISSING = "no credentials username";
private final String name;
/**
* Creates a new {@link DefaultDockerCredentialHelper} instance using the specified
* credential helper name.
* @param name the full name of the Docker credential helper, e.g.,
* {@code docker-credential-osxkeychain}, {@code docker-credential-desktop}, etc.
*/
DefaultDockerCredentialHelper(String name) {
this.name = name;
}
@Override
public Credentials get(String serverUrl) throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder().redirectErrorStream(true);
if (Platform.isWindows()) {
processBuilder.command("cmd", "/c");
}
processBuilder.command(this.name, "get");
Process process = startProcess(processBuilder);
try (OutputStream os = process.getOutputStream()) {
os.write(serverUrl.getBytes(StandardCharsets.UTF_8));
}
int exitCode;
try {
exitCode = process.waitFor();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return null;
}
if (exitCode != 0) {
try (InputStream is = process.getInputStream()) {
String message = new String(is.readAllBytes(), StandardCharsets.UTF_8);
if (isCredentialsNotFoundError(message)) {
return null;
}
throw new IOException("%s' exited with code %d: %s".formatted(process, exitCode, message));
}
}
try (InputStream is = process.getInputStream()) {
return new Credentials(SharedObjectMapper.get().readTree(is));
}
}
private Process startProcess(ProcessBuilder processBuilder) throws IOException {
try {
return processBuilder.start();
}
catch (IOException ex) {
if (!Platform.isMac()) {
throw ex;
}
List<String> command = new ArrayList<>(processBuilder.command());
command.set(0, USR_LOCAL_BIN + command.get(0));
return processBuilder.command(command).start();
}
}
private boolean isCredentialsNotFoundError(String message) {
return switch (message.trim()) {
case CREDENTIALS_NOT_FOUND, CREDENTIALS_URL_MISSING, CREDENTIALS_USERNAME_MISSING -> true;
default -> false;
};
}
}

View File

@ -0,0 +1,138 @@
/*
* 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.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Supplier;
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;
import org.springframework.util.function.SingletonSupplier;
/**
* A default implementation of {@link DockerRegistryAuthentication} that provides
* authentication using a Docker configuration file, leveraging both credential helpers
* and static credentials.
*
* @author Dmytro Nosan
*/
class DefaultDockerRegistryAuthentication implements DockerRegistryAuthentication {
private static final String DEFAULT_DOMAIN = "docker.io";
private static final String INDEX_URL = "https://index.docker.io/v1/";
private final Map<String, String> cache = new ConcurrentHashMap<>();
private final Function<String, DockerCredentialHelper> dockerCredentialHelperFactory;
private final Supplier<DockerConfig> dockerConfigSupplier;
DefaultDockerRegistryAuthentication() {
this(Environment.SYSTEM, DockerCredentialHelper::ofSuffix);
}
/**
* Creates a {@code DockerConfigFileDockerRegistryAuthentication} instance using the
* provided {@link Environment} and {@link DockerCredentialHelper} factory.
* @param environment the environment from which to retrieve environment variables
* @param dockerCredentialHelperFactory the factory to create a
* {@link DockerCredentialHelper} instance based on the provided credential helper
* name. The factory is invoked with the credential helper name. For example,
* {@code desktop}, {@code osxkeychain}, etc.
*/
DefaultDockerRegistryAuthentication(Environment environment,
Function<String, DockerCredentialHelper> dockerCredentialHelperFactory) {
this.dockerConfigSupplier = SingletonSupplier
.of(() -> DockerConfigurationMetadata.from(environment).getConfiguration());
this.dockerCredentialHelperFactory = dockerCredentialHelperFactory;
}
@Override
public String getAuthHeader(ImageReference imageReference) {
// TODO: Should the authentication header be cached? The Docker CLI does not cache
// it, whereas testcontainers does. Is caching safe in this context?
return this.cache.computeIfAbsent(getServerUrl(imageReference), (serverUrl) -> {
DockerConfig dockerConfig = this.dockerConfigSupplier.get();
return getAuthentication(dockerConfig, serverUrl).getAuthHeader(imageReference);
});
}
private String getServerUrl(ImageReference imageReference) {
String domain = imageReference.getDomain();
return DEFAULT_DOMAIN.equals(domain) ? INDEX_URL : domain;
}
private DockerRegistryAuthentication getAuthentication(DockerConfig dockerConfig, String serverUrl) {
RegistryAuth auth = getAuth(dockerConfig, serverUrl);
Credentials credentials = getCredentials(dockerConfig, serverUrl);
if (credentials != null && credentials.isIdentityToken()) {
return new DockerRegistryTokenAuthentication(credentials.getSecret());
}
if (credentials != null) {
return new DockerRegistryUserAuthentication(credentials.getUsername(), credentials.getSecret(),
(credentials.getServerUrl() != null) ? credentials.getServerUrl() : serverUrl,
(auth != null) ? auth.email() : null);
}
if (auth != null) {
return new DockerRegistryUserAuthentication(auth.username(), auth.password(), auth.serverUrl(),
auth.email());
}
return new DockerRegistryUserAuthentication("", "", "", "");
}
private Credentials getCredentials(DockerConfig dockerConfig, String serverUrl) {
try {
// TODO: The Docker CLI prioritizes the credential helper over the
// credential store, even when the helper is empty:
// https://github.com/docker/cli/blob/c8f9187157753e366ee9f25524b56f90913b47a5/cli/config/configfile/file.go#L280
// Should the credential store be considered if the credential helper is
// empty? The latter approach is followed by Testcontainers.
String helper = dockerConfig.getCredHelpers().getOrDefault(serverUrl, dockerConfig.getCredsStore());
return StringUtils.hasText(helper) ? this.dockerCredentialHelperFactory.apply(helper).get(serverUrl) : null;
}
catch (IOException ex) {
// TODO: Errors encountered while retrieving credentials from the
// credential helpers are ignored by the Docker CLI.
System.err.printf("Error retrieving credentials for '%s' due to: %s%n", serverUrl, ex.getMessage());
}
return null;
}
private RegistryAuth getAuth(DockerConfig dockerConfig, String serverUrl) {
return dockerConfig.getAuths()
.entrySet()
.stream()
.filter((entry) -> entry.getKey().equals(serverUrl) || entry.getKey().endsWith("://" + serverUrl))
.map((entry) -> new RegistryAuth(entry.getKey(), entry.getValue()))
.findFirst()
.orElse(null);
}
private record RegistryAuth(String serverUrl, String username, String password, String email) {
private RegistryAuth(String serverUrl, Auth auth) {
this(serverUrl, auth.getUsername(), auth.getPassword(), auth.getEmail());
}
}
}

View File

@ -94,6 +94,11 @@ public final class DockerConfiguration {
this.publishAuthentication, this.bindHostToBuilder);
}
public DockerConfiguration withBuilderRegistryDefaultAuthentication() {
return new DockerConfiguration(this.host, new DefaultDockerRegistryAuthentication(), this.publishAuthentication,
this.bindHostToBuilder);
}
public DockerConfiguration withPublishRegistryTokenAuthentication(String token) {
Assert.notNull(token, "'token' must not be null");
return new DockerConfiguration(this.host, this.builderAuthentication,
@ -113,6 +118,11 @@ public final class DockerConfiguration {
new DockerRegistryUserAuthentication("", "", "", ""), this.bindHostToBuilder);
}
public DockerConfiguration withPublishRegistryDefaultAuthentication() {
return new DockerConfiguration(this.host, this.builderAuthentication, new DefaultDockerRegistryAuthentication(),
this.bindHostToBuilder);
}
public static class DockerHostConfiguration {
private final String address;

View File

@ -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,11 @@ 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.Collections;
import java.util.HexFormat;
import java.util.LinkedHashMap;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
@ -148,15 +152,50 @@ final class DockerConfigurationMetadata {
private final String currentContext;
private final String credsStore;
private final Map<String, String> credHelpers;
private final Map<String, Auth> auths;
private DockerConfig(JsonNode node) {
super(node, MethodHandles.lookup());
this.currentContext = valueAt("/currentContext", String.class);
this.credsStore = valueAt("/credsStore", String.class);
this.credHelpers = extractCredHelpers();
this.auths = extractAuths();
}
private Map<String, Auth> extractAuths() {
Map<String, Auth> auths = new LinkedHashMap<>();
getNode().at("/auths")
.fields()
.forEachRemaining((entry) -> auths.put(entry.getKey(), new Auth(entry.getValue())));
return Map.copyOf(auths);
}
@SuppressWarnings("unchecked")
private Map<String, String> extractCredHelpers() {
Map<String, String> credHelpers = valueAt("/credHelpers", Map.class);
return (credHelpers != null) ? Map.copyOf(credHelpers) : Collections.emptyMap();
}
String getCurrentContext() {
return this.currentContext;
}
String getCredsStore() {
return this.credsStore;
}
Map<String, String> getCredHelpers() {
return this.credHelpers;
}
Map<String, Auth> getAuths() {
return this.auths;
}
static DockerConfig fromJson(String json) throws JsonProcessingException {
return new DockerConfig(SharedObjectMapper.get().readTree(json));
}
@ -167,6 +206,45 @@ 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 username = valueAt("/username", String.class);
String password = valueAt("/password", String.class);
String auth = valueAt("/auth", String.class);
if (auth != null) {
String[] parts = new String(Base64.getDecoder().decode(auth)).split(":", 2);
if (parts.length == 2) {
username = parts[0];
password = parts[1];
}
}
this.username = username;
this.password = password;
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;

View File

@ -0,0 +1,48 @@
/*
* 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;
/**
* Docker credential helper used to retrieve credentials for servers.
*
* @author Dmytro Nosan
*/
interface DockerCredentialHelper {
/**
* Retrieves the credential associated with the specified URL.
* @param serverUrl the server URL for which the credential is requested
* @return the {@link Credentials} containing authentication information for the given
* server, or {@code null} if no credential is available for the given server.
* @throws IOException if an I/O error occurs while retrieving the credential.
*/
Credentials get(String serverUrl) throws IOException;
/**
* Creates a {@link DockerCredentialHelper} instance using the specified suffix.
* @param suffix the suffix of the credential helper, for example {@code gcr},
* {@code ecr-login}, {@code desktop}, {@code osxkeychain}, etc.
* @return a {@link DefaultDockerCredentialHelper} instance, with the full name of the
* helper. e.g., {@code docker-credential-gcr}
*/
static DockerCredentialHelper ofSuffix(String suffix) {
return new DefaultDockerCredentialHelper("docker-credential-" + suffix.trim());
}
}

View File

@ -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.
@ -16,6 +16,8 @@
package org.springframework.boot.buildpack.platform.docker.configuration;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
/**
* Docker registry authentication configuration.
*
@ -27,7 +29,26 @@ public interface DockerRegistryAuthentication {
/**
* Returns the auth header that should be used for docker authentication.
* @return the auth header
* @throws IllegalStateException if the auth header cannot be created
* @deprecated since 3.5.0 for removal in 4.0.0 in favor of
* {@link #getAuthHeader(ImageReference)}
*
*/
String getAuthHeader();
@Deprecated(since = "3.5.0", forRemoval = true)
default String getAuthHeader() {
throw new IllegalStateException("Use getAuthHeader(ImageReference) instead of getAuthHeader()");
}
/**
* Returns the auth header that should be used for docker authentication.
* @param imageReference the image reference
* @return the auth header
* @throws IllegalStateException if the auth header cannot be created
* @since 3.5.0
*
*/
default String getAuthHeader(ImageReference imageReference) {
return getAuthHeader();
}
}

View File

@ -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.
@ -21,6 +21,7 @@ import java.util.Base64;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.json.SharedObjectMapper;
/**
@ -35,6 +36,12 @@ class JsonEncodedDockerRegistryAuthentication implements DockerRegistryAuthentic
private String authHeader;
@Override
public String getAuthHeader(ImageReference imageReference) {
return this.authHeader;
}
@Override
@SuppressWarnings("removal")
public String getAuthHeader() {
return this.authHeader;
}

View File

@ -142,11 +142,14 @@ class BuilderTests {
.withPublishRegistryTokenAuthentication("publish token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
eq(dockerConfiguration.getBuilderRegistryAuthentication()
.getAuthHeader(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)))))
.willAnswer(withPulledImage(builderImage));
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication()
.getAuthHeader(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
BuildRequest request = getTestRequest().withPublish(true);
@ -156,13 +159,15 @@ class BuilderTests {
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
eq(dockerConfiguration.getBuilderRegistryAuthentication()
.getAuthHeader(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF))));
then(docker.image()).should()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication()
.getAuthHeader(ImageReference.of("docker.io/cloudfoundry/run:base-cnb"))));
then(docker.image()).should()
.push(eq(request.getName()), any(),
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader(request.getName())));
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
then(docker.image()).shouldHaveNoMoreInteractions();
@ -388,11 +393,14 @@ class BuilderTests {
.withPublishRegistryTokenAuthentication("publish token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
eq(dockerConfiguration.getBuilderRegistryAuthentication()
.getAuthHeader(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)))))
.willAnswer(withPulledImage(builderImage));
given(docker.image()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication()
.getAuthHeader(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")))))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
BuildRequest request = getTestRequest().withPublish(true).withTags(ImageReference.of("my-application:1.2.3"));
@ -403,17 +411,20 @@ class BuilderTests {
then(docker.image()).should()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), isNull(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
eq(dockerConfiguration.getBuilderRegistryAuthentication()
.getAuthHeader(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF))));
then(docker.image()).should()
.pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), eq(ImagePlatform.from(builderImage)),
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
any(), eq(dockerConfiguration.getBuilderRegistryAuthentication()
.getAuthHeader(ImageReference.of("docker.io/cloudfoundry/run:base-cnb"))));
then(docker.image()).should()
.push(eq(request.getName()), any(),
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader(request.getName())));
then(docker.image()).should().tag(eq(request.getName()), eq(ImageReference.of("my-application:1.2.3")));
then(docker.image()).should()
.push(eq(ImageReference.of("my-application:1.2.3")), any(),
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
eq(dockerConfiguration.getPublishRegistryAuthentication()
.getAuthHeader(ImageReference.of("my-application:1.2.3"))));
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
then(docker.image()).should().load(archive.capture(), any());
then(docker.image()).should().remove(archive.getValue().getTag(), true);
@ -487,42 +498,6 @@ class BuilderTests {
.withMessage("Builder lifecycle 'creator' failed with status code 9");
}
@Test
void buildWhenDetectedRunImageInDifferentAuthenticatedRegistryThrowsException() throws Exception {
TestPrintStream out = new TestPrintStream();
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image-with-run-image-different-registry.json");
DockerConfiguration dockerConfiguration = new DockerConfiguration()
.withBuilderRegistryTokenAuthentication("builder token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(builderImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
BuildRequest request = getTestRequest();
assertThatIllegalStateException().isThrownBy(() -> builder.build(request))
.withMessage(
"Run image 'example.com/custom/run:latest' must be pulled from the 'docker.io' authenticated registry");
}
@Test
void buildWhenRequestedRunImageInDifferentAuthenticatedRegistryThrowsException() throws Exception {
TestPrintStream out = new TestPrintStream();
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
DockerConfiguration dockerConfiguration = new DockerConfiguration()
.withBuilderRegistryTokenAuthentication("builder token");
given(docker.image()
.pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_REF)), any(), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(builderImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
BuildRequest request = getTestRequest().withRunImage(ImageReference.of("example.com/custom/run:latest"));
assertThatIllegalStateException().isThrownBy(() -> builder.build(request))
.withMessage(
"Run image 'example.com/custom/run:latest' must be pulled from the 'docker.io' authenticated registry");
}
@Test
void buildWhenRequestedBuildpackNotInBuilderThrowsException() throws Exception {
TestPrintStream out = new TestPrintStream();

View File

@ -0,0 +1,75 @@
/*
* 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 Credentials}.
*
* @author Dmytro Nosan
*/
class CredentialsTests {
@Test
@WithResource(name = "credentials.json", content = """
{
"ServerURL": "https://index.docker.io/v1/",
"Username": "<token>",
"Secret": "secret"
}
""")
void shouldCreateIdentityTokenCredentials() throws IOException {
Credentials credentials = getCredentials("credentials.json");
assertThat(credentials.getUsername()).isEqualTo("<token>");
assertThat(credentials.getSecret()).isEqualTo("secret");
assertThat(credentials.getServerUrl()).isEqualTo("https://index.docker.io/v1/");
assertThat(credentials.isIdentityToken()).isTrue();
}
@Test
@WithResource(name = "credentials.json", content = """
{
"ServerURL": "https://index.docker.io/v1/",
"Username": "user",
"Secret": "secret"
}
""")
void shouldCreateUsernamePasswordCredentials() throws IOException {
Credentials 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();
}
private Credentials getCredentials(String name) throws IOException {
try (InputStream inputStream = new ClassPathResource(name).getInputStream()) {
return new Credentials(SharedObjectMapper.get().readTree(inputStream));
}
}
}

View File

@ -0,0 +1,101 @@
/*
* 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.UUID;
import com.sun.jna.Platform;
import org.junit.jupiter.api.BeforeEach;
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 DefaultDockerCredentialHelper}.
*
* @author Dmytro Nosan
*/
class DefaultDockerCredentialHelperTests {
private DefaultDockerCredentialHelper helper;
@BeforeEach
void setUp() throws IOException {
String name = "docker-credential-test";
if (Platform.isWindows()) {
name += ".bat";
}
this.helper = new DefaultDockerCredentialHelper(
new ClassPathResource(name, getClass()).getFile().getAbsolutePath());
}
@Test
void shouldReturnCredentialsForUser() throws IOException {
Credentials credentials = this.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 shouldReturnCredentialsForToken() throws IOException {
Credentials credentials = this.helper.get("token.example.com");
assertThat(credentials).isNotNull();
assertThat(credentials.isIdentityToken()).isTrue();
assertThat(credentials.getServerUrl()).isEqualTo("token.example.com");
assertThat(credentials.getUsername()).isEqualTo("<token>");
assertThat(credentials.getSecret()).isEqualTo("secret");
}
@Test
void shouldReturnNullCredentialsWhenCredentialsNotFoundError() throws IOException {
Credentials credentials = this.helper.get("credentials.missing.example.com");
assertThat(credentials).isNull();
}
@Test
void shouldReturnNullCredentialsWhenUsernameMissingError() throws IOException {
Credentials credentials = this.helper.get("username.missing.example.com");
assertThat(credentials).isNull();
}
@Test
void shouldReturnNullCredentialsWhenServerUrlMissingError() throws IOException {
Credentials credentials = this.helper.get("url.missing.example.com");
assertThat(credentials).isNull();
}
@Test
void shouldThrowIOExceptionWhenUnknownError() {
assertThatIOException().isThrownBy(() -> this.helper.get("invalid.example.com"))
.withMessageContaining("Unknown error");
}
@Test
void shouldThrowIOExceptionWhenCommandDoesNotExist() {
String name = "docker-credential-%s".formatted(UUID.randomUUID().toString());
assertThatIOException().isThrownBy(() -> new DefaultDockerCredentialHelper(name).get("invalid.example.com"))
.withMessageContaining(name);
}
}

View File

@ -0,0 +1,337 @@
/*
* 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.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.CapturedOutput;
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 DefaultDockerRegistryAuthentication}.
*
* @author Dmytro Nosan
*/
@ExtendWith(OutputCaptureExtension.class)
class DefaultDockerRegistryAuthenticationTests {
private final Map<String, String> environment = new LinkedHashMap<>();
private final Map<String, DockerCredentialHelper> dockerCredentialHelpers = new LinkedHashMap<>();
private final DefaultDockerRegistryAuthentication authentication = new DefaultDockerRegistryAuthentication(
this.environment::get, this.dockerCredentialHelpers::get);
@WithResource(name = "config.json", content = """
{
"auths": {
"https://index.docker.io/v1/": {
"auth": "dXNlcm5hbWU6cGFzc3dvcmQ=",
"email": "test@gmail.com"
}
}
}
""")
@Test
void shouldCreateAuthHeaderFromAuthForDockerDomain(@ResourcesRoot Path directory) throws IOException {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("docker.io/ubuntu:latest");
String authHeader = this.authentication.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 shouldCreateAuthHeaderFromAuthForLegacyDockerDomain(@ResourcesRoot Path directory) throws IOException {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("index.docker.io/ubuntu:latest");
String authHeader = this.authentication.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 shouldCreateAuthHeaderFromAuthForCustomDomain(@ResourcesRoot Path directory) throws IOException {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("my-registry.example.com/ubuntu:latest");
String authHeader = this.authentication.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 shouldCreateAuthHeaderFromAuthForCustomDomainWithLegacyFormat(@ResourcesRoot Path directory)
throws IOException {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("my-registry.example.com/ubuntu:latest");
String authHeader = this.authentication.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 shouldCreateAuthHeaderFromEmptyCredentialsWhenEmptyConfig(@ResourcesRoot Path directory) throws IOException {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("docker.io/ubuntu:latest");
String authHeader = this.authentication.getAuthHeader(imageReference);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "")
.containsEntry("username", "")
.containsEntry("password", "")
.containsEntry("email", "");
}
@WithResource(name = "config.json", content = """
{
"credsStore": "desktop"
}
""")
@WithResource(name = "credentials.json", content = """
{
"ServerURL": "https://index.docker.io/v1/",
"Username": "<token>",
"Secret": "secret"
}
""")
@Test
void shouldCreateAuthHeaderFromCredsStore(@ResourcesRoot Path directory) throws IOException {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("docker.io/ubuntu:latest");
DockerCredentialHelper helper = mock(DockerCredentialHelper.class);
this.dockerCredentialHelpers.put("desktop", helper);
given(helper.get("https://index.docker.io/v1/")).willReturn(getCredentials("credentials.json"));
String authHeader = this.authentication.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 shouldCreateAuthHeaderFromCredHelperAndUseEmailFromAuth(@ResourcesRoot Path directory) throws IOException {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
DockerCredentialHelper helper = mock(DockerCredentialHelper.class);
this.dockerCredentialHelpers.put("gcr", helper);
given(helper.get("gcr.io")).willReturn(getCredentials("credentials.json"));
String authHeader = this.authentication.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 shouldCreateAuthHeaderFromCredHelperAndUseProvidedServerUrl(@ResourcesRoot Path directory) throws IOException {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
DockerCredentialHelper helper = mock(DockerCredentialHelper.class);
this.dockerCredentialHelpers.put("gcr", helper);
given(helper.get("gcr.io")).willReturn(getCredentials("credentials.json"));
String authHeader = this.authentication.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 shouldCreateHeaderFromAuthWhenFailedToGetCredentials(@ResourcesRoot Path directory, CapturedOutput output)
throws IOException {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
DockerCredentialHelper helper = mock(DockerCredentialHelper.class);
this.dockerCredentialHelpers.put("gcr", helper);
given(helper.get("gcr.io")).willThrow(new IOException("Failed to obtain credentials for registry"));
String authHeader = this.authentication.getAuthHeader(imageReference);
assertThat(output.getErr())
.contains("Error retrieving credentials for 'gcr.io' due to: Failed to obtain credentials for registry");
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "gcr.io")
.containsEntry("username", "username")
.containsEntry("password", "password")
.containsEntry("email", "test@gmail.com");
}
@WithResource(name = "config.json", content = """
{
"credsStore": "desktop",
"credHelpers": {
"gcr.io": "gcr"
}
}
""")
@Test
void shouldCreateEmptyAuthHeaderWhenFailedToGetCredentials(@ResourcesRoot Path directory, CapturedOutput output)
throws IOException {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
DockerCredentialHelper helper = mock(DockerCredentialHelper.class);
this.dockerCredentialHelpers.put("gcr", helper);
given(helper.get("gcr.io")).willThrow(new IOException("Failed to obtain credentials for registry"));
String authHeader = this.authentication.getAuthHeader(imageReference);
assertThat(output.getErr())
.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 emptyCredHelperShouldOverrideCredStore(@ResourcesRoot Path directory) throws IOException {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
String authHeader = this.authentication.getAuthHeader(imageReference);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "")
.containsEntry("username", "")
.containsEntry("password", "")
.containsEntry("email", "");
}
private Credentials getCredentials(String name) throws IOException {
try (InputStream inputStream = new ClassPathResource(name).getInputStream()) {
return new Credentials(SharedObjectMapper.get().readTree(inputStream));
}
}
private Map<String, String> decode(String authHeader) throws IOException {
assertThat(authHeader).isNotNull();
return SharedObjectMapper.get().readValue(Base64.getDecoder().decode(authHeader), new TypeReference<>() {
});
}
}

View File

@ -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;
@ -46,6 +47,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 +60,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 +73,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 +105,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();

View File

@ -0,0 +1,36 @@
/*
* 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 org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DockerCredentialHelper}.
*
* @author Dmytro Nosan
*/
class DockerCredentialHelperTests {
@Test
void shouldCreateDockerCredentialOfSuffix() {
DockerCredentialHelper helper = DockerCredentialHelper.ofSuffix("desktop");
assertThat(helper).hasFieldOrPropertyWithValue("name", "docker-credential-desktop");
}
}

View File

@ -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.
@ -24,6 +24,7 @@ import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import org.springframework.util.StreamUtils;
@ -37,7 +38,7 @@ class DockerRegistryTokenAuthenticationTests extends AbstractJsonTests {
@Test
void createAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
DockerRegistryTokenAuthentication auth = new DockerRegistryTokenAuthentication("tokenvalue");
String header = auth.getAuthHeader();
String header = auth.getAuthHeader(ImageReference.of("ubuntu:18.04"));
String expectedJson = StreamUtils.copyToString(getContent("auth-token.json"), StandardCharsets.UTF_8);
JSONAssert.assertEquals(expectedJson, new String(Base64.getUrlDecoder().decode(header)), true);
}

View File

@ -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.
@ -24,6 +24,7 @@ import org.json.JSONException;
import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.buildpack.platform.json.AbstractJsonTests;
import org.springframework.util.StreamUtils;
@ -34,17 +35,21 @@ import org.springframework.util.StreamUtils;
*/
class DockerRegistryUserAuthenticationTests extends AbstractJsonTests {
private final ImageReference imageReference = ImageReference.of("ubuntu:18.04");
@Test
void createMinimalAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
DockerRegistryUserAuthentication auth = new DockerRegistryUserAuthentication("user", "secret",
"https://docker.example.com", "docker@example.com");
JSONAssert.assertEquals(jsonContent("auth-user-full.json"), decoded(auth.getAuthHeader()), true);
JSONAssert.assertEquals(jsonContent("auth-user-full.json"), decoded(auth.getAuthHeader(this.imageReference)),
true);
}
@Test
void createFullAuthHeaderReturnsEncodedHeader() throws IOException, JSONException {
DockerRegistryUserAuthentication auth = new DockerRegistryUserAuthentication("user", "secret", null, null);
JSONAssert.assertEquals(jsonContent("auth-user-minimal.json"), decoded(auth.getAuthHeader()), false);
JSONAssert.assertEquals(jsonContent("auth-user-minimal.json"), decoded(auth.getAuthHeader(this.imageReference)),
false);
}
private String jsonContent(String s) throws IOException {

View File

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

View File

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

View File

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

View File

@ -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.
@ -145,7 +145,7 @@ public abstract class DockerSpec {
private DockerConfiguration customizeBuilderAuthentication(DockerConfiguration dockerConfiguration) {
if (this.builderRegistry == null || this.builderRegistry.hasEmptyAuth()) {
return dockerConfiguration;
return dockerConfiguration.withBuilderRegistryDefaultAuthentication();
}
if (this.builderRegistry.hasTokenAuth() && !this.builderRegistry.hasUserAuth()) {
return dockerConfiguration.withBuilderRegistryTokenAuthentication(this.builderRegistry.getToken().get());
@ -161,7 +161,7 @@ public abstract class DockerSpec {
private DockerConfiguration customizePublishAuthentication(DockerConfiguration dockerConfiguration) {
if (this.publishRegistry == null || this.publishRegistry.hasEmptyAuth()) {
return dockerConfiguration.withEmptyPublishRegistryAuthentication();
return dockerConfiguration.withPublishRegistryDefaultAuthentication();
}
if (this.publishRegistry.hasTokenAuth() && !this.publishRegistry.hasUserAuth()) {
return dockerConfiguration.withPublishRegistryTokenAuthentication(this.publishRegistry.getToken().get());

View File

@ -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,7 +26,9 @@ import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.boot.gradle.junit.GradleProjectBuilder;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@ -39,6 +41,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*/
class DockerSpecTests {
private final ImageReference imageReference = ImageReference.of("ubuntu:18.04");
private DockerSpec dockerSpec;
@BeforeEach
@ -54,12 +58,10 @@ class DockerSpecTests {
void asDockerConfigurationWithDefaults() {
DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration();
assertThat(dockerConfiguration.getHost()).isNull();
assertThat(dockerConfiguration.getBuilderRegistryAuthentication()).isNull();
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
.contains("\"email\" : \"\"")
.contains("\"serveraddress\" : \"\"");
assertThat(dockerConfiguration.getBuilderRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
assertThat(dockerConfiguration.getPublishRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
}
@Test
@ -74,12 +76,10 @@ class DockerSpecTests {
assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert");
assertThat(host.getContext()).isNull();
assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse();
assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
.contains("\"email\" : \"\"")
.contains("\"serveraddress\" : \"\"");
assertThat(dockerConfiguration.getBuilderRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
assertThat(dockerConfiguration.getPublishRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
}
@Test
@ -92,12 +92,10 @@ class DockerSpecTests {
assertThat(host.getCertificatePath()).isNull();
assertThat(host.getContext()).isNull();
assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse();
assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
.contains("\"email\" : \"\"")
.contains("\"serveraddress\" : \"\"");
assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
assertThat(dockerConfiguration.getPublishRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
}
@Test
@ -110,12 +108,10 @@ class DockerSpecTests {
assertThat(host.isSecure()).isFalse();
assertThat(host.getCertificatePath()).isNull();
assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse();
assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
.contains("\"email\" : \"\"")
.contains("\"serveraddress\" : \"\"");
assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
assertThat(dockerConfiguration.getPublishRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
}
@Test
@ -136,12 +132,10 @@ class DockerSpecTests {
assertThat(host.isSecure()).isFalse();
assertThat(host.getCertificatePath()).isNull();
assertThat(dockerConfiguration.isBindHostToBuilder()).isTrue();
assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication()).isNull();
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
.contains("\"email\" : \"\"")
.contains("\"serveraddress\" : \"\"");
assertThat(this.dockerSpec.asDockerConfiguration().getBuilderRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
assertThat(dockerConfiguration.getPublishRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
}
@Test
@ -159,12 +153,12 @@ class DockerSpecTests {
registry.getEmail().set("docker2@example.com");
});
DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration();
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader(this.imageReference)))
.contains("\"username\" : \"user1\"")
.contains("\"password\" : \"secret1\"")
.contains("\"email\" : \"docker1@example.com\"")
.contains("\"serveraddress\" : \"https://docker1.example.com\"");
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader(this.imageReference)))
.contains("\"username\" : \"user2\"")
.contains("\"password\" : \"secret2\"")
.contains("\"email\" : \"docker2@example.com\"")
@ -199,9 +193,9 @@ class DockerSpecTests {
this.dockerSpec.builderRegistry((registry) -> registry.getToken().set("token1"));
this.dockerSpec.publishRegistry((registry) -> registry.getToken().set("token2"));
DockerConfiguration dockerConfiguration = this.dockerSpec.asDockerConfiguration();
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader(this.imageReference)))
.contains("\"identitytoken\" : \"token1\"");
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader(this.imageReference)))
.contains("\"identitytoken\" : \"token2\"");
}
@ -220,4 +214,10 @@ class DockerSpecTests {
return new String(Base64.getDecoder().decode(value));
}
private Class<?> getDockerRegistryConfigFileAuthenticationClass() {
return ClassUtils.resolveClassName(
"org.springframework.boot.buildpack.platform.docker.configuration.DefaultDockerRegistryAuthentication",
getClass().getClassLoader());
}
}

View File

@ -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.
@ -168,7 +168,7 @@ public class Docker {
private DockerConfiguration customizeBuilderAuthentication(DockerConfiguration dockerConfiguration) {
if (this.builderRegistry == null || this.builderRegistry.isEmpty()) {
return dockerConfiguration;
return dockerConfiguration.withBuilderRegistryDefaultAuthentication();
}
if (this.builderRegistry.hasTokenAuth() && !this.builderRegistry.hasUserAuth()) {
return dockerConfiguration.withBuilderRegistryTokenAuthentication(this.builderRegistry.getToken());
@ -187,7 +187,7 @@ public class Docker {
return dockerConfiguration;
}
if (this.publishRegistry == null || this.publishRegistry.isEmpty()) {
return dockerConfiguration.withEmptyPublishRegistryAuthentication();
return dockerConfiguration.withPublishRegistryDefaultAuthentication();
}
if (this.publishRegistry.hasTokenAuth() && !this.publishRegistry.hasUserAuth()) {
return dockerConfiguration.withPublishRegistryTokenAuthentication(this.publishRegistry.getToken());

View File

@ -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.
@ -22,6 +22,8 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration;
import org.springframework.boot.buildpack.platform.docker.configuration.DockerConfiguration.DockerHostConfiguration;
import org.springframework.boot.buildpack.platform.docker.type.ImageReference;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -34,17 +36,17 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*/
class DockerTests {
private final ImageReference imageReference = ImageReference.of("ubuntu:22.04");
@Test
void asDockerConfigurationWithDefaults() {
void asDockerConfigurationWithDefaults() throws ClassNotFoundException {
Docker docker = new Docker();
DockerConfiguration dockerConfiguration = createDockerConfiguration(docker);
assertThat(dockerConfiguration.getHost()).isNull();
assertThat(dockerConfiguration.getBuilderRegistryAuthentication()).isNull();
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
.contains("\"email\" : \"\"")
.contains("\"serveraddress\" : \"\"");
assertThat(dockerConfiguration.getBuilderRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
assertThat(dockerConfiguration.getPublishRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
}
@Test
@ -60,12 +62,10 @@ class DockerTests {
assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert");
assertThat(host.getContext()).isNull();
assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse();
assertThat(createDockerConfiguration(docker).getBuilderRegistryAuthentication()).isNull();
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
.contains("\"email\" : \"\"")
.contains("\"serveraddress\" : \"\"");
assertThat(dockerConfiguration.getBuilderRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
assertThat(dockerConfiguration.getPublishRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
}
@Test
@ -79,12 +79,10 @@ class DockerTests {
assertThat(host.isSecure()).isFalse();
assertThat(host.getCertificatePath()).isNull();
assertThat(dockerConfiguration.isBindHostToBuilder()).isFalse();
assertThat(createDockerConfiguration(docker).getBuilderRegistryAuthentication()).isNull();
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
.contains("\"email\" : \"\"")
.contains("\"serveraddress\" : \"\"");
assertThat(dockerConfiguration.getBuilderRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
assertThat(dockerConfiguration.getPublishRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
}
@Test
@ -109,12 +107,10 @@ class DockerTests {
assertThat(host.isSecure()).isTrue();
assertThat(host.getCertificatePath()).isEqualTo("/tmp/ca-cert");
assertThat(dockerConfiguration.isBindHostToBuilder()).isTrue();
assertThat(createDockerConfiguration(docker).getBuilderRegistryAuthentication()).isNull();
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
.contains("\"username\" : \"\"")
.contains("\"password\" : \"\"")
.contains("\"email\" : \"\"")
.contains("\"serveraddress\" : \"\"");
assertThat(createDockerConfiguration(docker).getBuilderRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
assertThat(dockerConfiguration.getPublishRegistryAuthentication())
.isInstanceOf(getDockerRegistryConfigFileAuthenticationClass());
}
@Test
@ -125,12 +121,12 @@ class DockerTests {
docker.setPublishRegistry(
new Docker.DockerRegistry("user2", "secret2", "https://docker2.example.com", "docker2@example.com"));
DockerConfiguration dockerConfiguration = createDockerConfiguration(docker);
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader(this.imageReference)))
.contains("\"username\" : \"user1\"")
.contains("\"password\" : \"secret1\"")
.contains("\"email\" : \"docker1@example.com\"")
.contains("\"serveraddress\" : \"https://docker1.example.com\"");
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader(this.imageReference)))
.contains("\"username\" : \"user2\"")
.contains("\"password\" : \"secret2\"")
.contains("\"email\" : \"docker2@example.com\"")
@ -170,9 +166,9 @@ class DockerTests {
docker.setBuilderRegistry(new Docker.DockerRegistry("token1"));
docker.setPublishRegistry(new Docker.DockerRegistry("token2"));
DockerConfiguration dockerConfiguration = createDockerConfiguration(docker);
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()))
assertThat(decoded(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader(this.imageReference)))
.contains("\"identitytoken\" : \"token1\"");
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()))
assertThat(decoded(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader(this.imageReference)))
.contains("\"identitytoken\" : \"token2\"");
}
@ -205,6 +201,12 @@ class DockerTests {
}
private Class<?> getDockerRegistryConfigFileAuthenticationClass() {
return ClassUtils.resolveClassName(
"org.springframework.boot.buildpack.platform.docker.configuration.DefaultDockerRegistryAuthentication",
getClass().getClassLoader());
}
String decoded(String value) {
return new String(Base64.getDecoder().decode(value));
}