diff --git a/docker_auth.go b/docker_auth.go index af0d415de9..58b3ef2637 100644 --- a/docker_auth.go +++ b/docker_auth.go @@ -21,6 +21,9 @@ import ( // defaultRegistryFn is variable overwritten in tests to check for behaviour with different default values. var defaultRegistryFn = defaultRegistry +// getRegistryCredentials is a variable overwritten in tests to mock the dockercfg.GetRegistryCredentials function. +var getRegistryCredentials = dockercfg.GetRegistryCredentials + // DockerImageAuth returns the auth config for the given Docker image, extracting first its Docker registry. // Finally, it will use the credential helpers to extract the information from the docker config file // for that registry, if it exists. @@ -111,9 +114,28 @@ type credentials struct { var creds = &credentialsCache{entries: map[string]credentials{}} -// Get returns the username and password for the given hostname +// AuthConfig updates the details in authConfig for the given hostname +// as determined by the details in configKey. +func (c *credentialsCache) AuthConfig(hostname, configKey string, authConfig *registry.AuthConfig) error { + u, p, err := creds.get(hostname, configKey) + if err != nil { + return err + } + + if u != "" { + authConfig.Username = u + authConfig.Password = p + } else { + authConfig.IdentityToken = p + } + + return nil +} + +// get returns the username and password for the given hostname // as determined by the details in configPath. -func (c *credentialsCache) Get(hostname, configKey string) (string, string, error) { +// If the username is empty, the password is an identity token. +func (c *credentialsCache) get(hostname, configKey string) (string, string, error) { key := configKey + ":" + hostname c.mtx.RLock() entry, ok := c.entries[key] @@ -124,7 +146,7 @@ func (c *credentialsCache) Get(hostname, configKey string) (string, string, erro } // No entry found, request and cache. - user, password, err := dockercfg.GetRegistryCredentials(hostname) + user, password, err := getRegistryCredentials(hostname) if err != nil { return "", "", fmt.Errorf("getting credentials for %s: %w", hostname, err) } @@ -186,14 +208,10 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) { switch { case ac.Username == "" && ac.Password == "": // Look up credentials from the credential store. - u, p, err := creds.Get(k, key) - if err != nil { + if err := creds.AuthConfig(k, key, &ac); err != nil { results <- authConfigResult{err: err} return } - - ac.Username = u - ac.Password = p case ac.Auth == "": // Create auth from the username and password encoding. ac.Auth = base64.StdEncoding.EncodeToString([]byte(ac.Username + ":" + ac.Password)) @@ -203,25 +221,19 @@ func getDockerAuthConfigs() (map[string]registry.AuthConfig, error) { }(k, v) } - // in the case where the auth field in the .docker/conf.json is empty, and the user has credential helpers registered - // the auth comes from there + // In the case where the auth field in the .docker/conf.json is empty, and the user has + // credential helpers registered the auth comes from there. for k := range cfg.CredentialHelpers { go func(k string) { defer wg.Done() - u, p, err := creds.Get(k, key) - if err != nil { + var ac registry.AuthConfig + if err := creds.AuthConfig(k, key, &ac); err != nil { results <- authConfigResult{err: err} return } - results <- authConfigResult{ - key: k, - cfg: registry.AuthConfig{ - Username: u, - Password: p, - }, - } + results <- authConfigResult{key: k, cfg: ac} }(k) } diff --git a/docker_auth_test.go b/docker_auth_test.go index 22c86783a2..8f18a62d9d 100644 --- a/docker_auth_test.go +++ b/docker_auth_test.go @@ -20,14 +20,18 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) -const exampleAuth = "https://example-auth.com" +const ( + exampleAuth = "https://example-auth.com" + privateRegistry = "https://my.private.registry" + exampleRegistry = "https://example.com" +) func Test_getDockerConfig(t *testing.T) { expectedConfig := &dockercfg.Config{ AuthConfigs: map[string]dockercfg.AuthConfig{ - core.IndexDockerIO: {}, - "https://example.com": {}, - "https://my.private.registry": {}, + core.IndexDockerIO: {}, + exampleRegistry: {}, + privateRegistry: {}, }, CredentialsStore: "desktop", } @@ -378,6 +382,13 @@ func localAddress(t *testing.T) string { //go:embed testdata/.docker/config.json var dockerConfig string +// reset resets the credentials cache. +func (c *credentialsCache) reset() { + c.mtx.Lock() + defer c.mtx.Unlock() + c.entries = make(map[string]credentials) +} + func Test_getDockerAuthConfigs(t *testing.T) { t.Run("HOME/valid", func(t *testing.T) { testDockerConfigHome(t, "testdata") @@ -418,6 +429,45 @@ func Test_getDockerAuthConfigs(t *testing.T) { require.Nil(t, authConfigs) }) + t.Run("DOCKER_AUTH_CONFIG/identity-token", func(t *testing.T) { + testDockerConfigHome(t, "testdata", "not-exist") + + // Reset the credentials cache to ensure our mocked method is called. + creds.reset() + + // Mock getRegistryCredentials to return identity-token for index.docker.io. + old := getRegistryCredentials + t.Cleanup(func() { + getRegistryCredentials = old + creds.reset() // Ensure our mocked results aren't cached. + }) + getRegistryCredentials = func(hostname string) (string, string, error) { + switch hostname { + case core.IndexDockerIO: + return "", "identity-token", nil + default: + return "username", "password", nil + } + } + t.Setenv("DOCKER_AUTH_CONFIG", dockerConfig) + + authConfigs, err := getDockerAuthConfigs() + require.NoError(t, err) + require.Equal(t, map[string]registry.AuthConfig{ + core.IndexDockerIO: { + IdentityToken: "identity-token", + }, + privateRegistry: { + Username: "username", + Password: "password", + }, + exampleRegistry: { + Username: "username", + Password: "password", + }, + }, authConfigs) + }) + t.Run("DOCKER_CONFIG/valid", func(t *testing.T) { testDockerConfigHome(t, "testdata", "not-found") t.Setenv("DOCKER_CONFIG", filepath.Join("testdata", ".docker")) @@ -445,9 +495,9 @@ func requireValidAuthConfig(t *testing.T) { // We can only check the keys as the values are not deterministic as they depend // on users environment. expected := map[string]registry.AuthConfig{ - "https://index.docker.io/v1/": {}, - "https://example.com": {}, - "https://my.private.registry": {}, + core.IndexDockerIO: {}, + exampleRegistry: {}, + privateRegistry: {}, } for k := range authConfigs { authConfigs[k] = registry.AuthConfig{}