Skip to content

DockerRegistryConfigAuthentication does not align with Docker CLI #45292

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@

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;
Expand Down Expand Up @@ -55,7 +53,7 @@ class DockerRegistryConfigAuthentication implements DockerRegistryAuthentication
DockerRegistryConfigAuthentication(DockerRegistryAuthentication fallback,
BiConsumer<String, Exception> credentialHelperExceptionHandler) {
this(fallback, credentialHelperExceptionHandler, Environment.SYSTEM,
(helper) -> new CredentialHelper("docker-credential-" + helper.trim()));
(helper) -> new CredentialHelper("docker-credential-" + helper));
}

DockerRegistryConfigAuthentication(DockerRegistryAuthentication fallback,
Expand Down Expand Up @@ -106,14 +104,14 @@ private DockerRegistryAuthentication getAuthentication(Credential credentialsFro
}
String username = credentialsFromHelper.getUsername();
String password = credentialsFromHelper.getSecret();
String serverAddress = (credentialsFromHelper.getServerUrl() != null
&& !credentialsFromHelper.getServerUrl().isEmpty()) ? credentialsFromHelper.getServerUrl() : serverUrl;
String serverAddress = (StringUtils.hasLength(credentialsFromHelper.getServerUrl()))
? 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))
return StringUtils.hasLength(serverUrl)
? credentialFromHelperCache.computeIfAbsent(serverUrl, this::computeCredentialsFromHelper) : null;
}

Expand All @@ -123,7 +121,7 @@ private Credential computeCredentialsFromHelper(String serverUrl) {
try {
return credentialHelper.get(serverUrl);
}
catch (IOException ex) {
catch (Exception ex) {
String message = "Error retrieving credentials for '%s' due to: %s".formatted(serverUrl,
ex.getMessage());
this.credentialHelperExceptionHandler.accept(message, ex);
Expand All @@ -134,10 +132,10 @@ private Credential computeCredentialsFromHelper(String serverUrl) {

private CredentialHelper getCredentialHelper(String serverUrl) {
String name = this.dockerConfig.getCredHelpers().getOrDefault(serverUrl, this.dockerConfig.getCredsStore());
return (name != null) ? this.credentialHelperFactory.apply(name.trim()) : null;
return (StringUtils.hasLength(name)) ? this.credentialHelperFactory.apply(name) : null;
Copy link
Contributor Author

@nosan nosan Apr 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docker CLI does not Trim the helper name:

func (configFile *ConfigFile) GetCredentialsStore(registryHostname string) credentials.Store {
	if helper := getConfiguredCredentialStore(configFile, registryHostname); helper != "" {
		return newNativeStore(configFile, helper)
	}
	return credentials.NewFileStore(configFile)
}

// var for unit testing.
var newNativeStore = func(configFile *ConfigFile, helperSuffix string) credentials.Store {
	return credentials.NewNativeStore(configFile, helperSuffix)
}


func NewNativeStore(file store, helperSuffix string) Store {
	name := remoteCredentialsPrefix + helperSuffix
	return &nativeStore{
		programFunc: client.NewShellProgramFunc(name),
		fileStore:   NewFileStore(file),
	}
}

I think trim() here looks logical, I can't imagine that credential helper would have whitespaces.

Copy link
Contributor Author

@nosan nosan Apr 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've verified this today once again with the following JSON

{
	"credHelpers": {
		"662409547778.dkr.ecr.eu-central-1.amazonaws.com": " ecr-login "
	}
}

And indeed, Docker CLI does not trim the helper; it fails to use the helper and falls back to basic auth.

The push refers to repository [662409547778.dkr.ecr.eu-central-1.amazonaws.com/gh-44633]
1dc94a70dbaa: Preparing
f11551f94b2b: Preparing
7130a16bceef: Preparing
41ee45b75d9f: Preparing
97d38fb9a19d: Preparing
508c281dc5cd: Preparing
09173eaeddc8: Waiting
1a6d2f237874: Waiting
c059b6f20445: Waiting
cdd4575ae9b3: Waiting
f0e9078fd509: Waiting
109d6909a2e0: Waiting
417e5bfc3c82: Waiting
a838c55de6ff: Waiting
bea0a3dc2651: Waiting
9c1f69b4e68a: Waiting
0560872d3bba: Waiting
e7cd92e3f4c6: Waiting
95305ea8b76a: Waiting
5953c33dbcf5: Waiting
no basic auth credentials

Everything worked fine when I removed any leading and trailing whitespace from a helper.

{
	"credHelpers": {
		"662409547778.dkr.ecr.eu-central-1.amazonaws.com": "ecr-login"
	}
}

The push refers to repository [662409547778.dkr.ecr.eu-central-1.amazonaws.com/gh-44633]
1dc94a70dbaa: Pushed
f11551f94b2b: Pushed
7130a16bceef: Pushed
41ee45b75d9f: Pushed
97d38fb9a19d: Pushed
508c281dc5cd: Pushed
09173eaeddc8: Pushed
1a6d2f237874: Pushed
c059b6f20445: Pushed
cdd4575ae9b3: Pushed
f0e9078fd509: Pushed
109d6909a2e0: Pushed
417e5bfc3c82: Pushed
a838c55de6ff: Pushed
bea0a3dc2651: Pushed
9c1f69b4e68a: Pushed
0560872d3bba: Pushed
e7cd92e3f4c6: Pushed
95305ea8b76a: Pushed
5953c33dbcf5: Pushed
latest: digest: sha256:65a43497ecee869b28f7a93b7b6f638e42c2fe91ecff395dc2fbedddd0b7f260 size: 4500

They appear to use the configurations provided by users as they are, except the "" string, which is the default value for the string type in Go.

}

private Entry<String, Auth> getAuthConfigEntry(String serverUrl) {
private Map.Entry<String, Auth> getAuthConfigEntry(String serverUrl) {
for (Map.Entry<String, Auth> candidate : this.dockerConfig.getAuths().entrySet()) {
if (candidate.getKey().equals(serverUrl) || candidate.getKey().endsWith("://" + serverUrl)) {
return candidate;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@
import org.springframework.core.io.ClassPathResource;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;

/**
* Tests for {@link DockerRegistryConfigAuthentication}.
Expand All @@ -53,7 +56,7 @@ class DockerRegistryConfigAuthenticationTests {

private final Map<String, Exception> helperExceptions = new LinkedHashMap<>();

private Map<String, CredentialHelper> credentialHelpers = new HashMap<>();
private final Map<String, CredentialHelper> credentialHelpers = new HashMap<>();

@BeforeEach
void cleanup() {
Expand Down Expand Up @@ -315,6 +318,7 @@ void getAuthHeaderWhenEmptyCredHelperReturnsFallbackAndDoesNotUseCredStore(@Reso
throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
ImageReference imageReference = ImageReference.of("gcr.io/ubuntu:latest");
CredentialHelper desktopHelper = mockHelper("desktop");
String authHeader = getAuthHeader(imageReference, DockerRegistryAuthentication.EMPTY_USER);
// The Docker CLI appears to prioritize the credential helper over the
// credential store, even when the helper is empty.
Expand All @@ -323,6 +327,25 @@ void getAuthHeaderWhenEmptyCredHelperReturnsFallbackAndDoesNotUseCredStore(@Reso
.containsEntry("username", "")
.containsEntry("password", "")
.containsEntry("email", "");
then(desktopHelper).should(never()).get(any(String.class));
}

@WithResource(name = "config.json", content = """
{
"credsStore": "desktop"
}
""")
@Test
void getAuthHeaderReturnsFallbackWhenImageReferenceNull(@ResourcesRoot Path directory) throws Exception {
this.environment.put("DOCKER_CONFIG", directory.toString());
CredentialHelper desktopHelper = mockHelper("desktop");
String authHeader = getAuthHeader(null, DockerRegistryAuthentication.EMPTY_USER);
assertThat(decode(authHeader)).hasSize(4)
.containsEntry("serveraddress", "")
.containsEntry("username", "")
.containsEntry("password", "")
.containsEntry("email", "");
then(desktopHelper).should(never()).get(any(String.class));
}

private String getAuthHeader(ImageReference imageReference) {
Expand Down