Skip to content
Draft
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
14 changes: 14 additions & 0 deletions cli/command/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package command

import (
"testing"

configtypes "github.com/docker/cli/cli/config/types"
)

func SetDesktopAuthConfig(t *testing.T, fn func(registryHostname string) (configtypes.AuthConfig, bool)) {
t.Helper()
orig := desktopAuthConfig
desktopAuthConfig = fn
t.Cleanup(func() { desktopAuthConfig = orig })
}
24 changes: 21 additions & 3 deletions cli/command/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/docker/cli/cli/hints"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/prompt"
"github.com/docker/cli/internal/secretsengine"
"github.com/docker/cli/internal/tui"
"github.com/moby/moby/api/pkg/authconfig"
registrytypes "github.com/moby/moby/api/types/registry"
Expand All @@ -35,6 +36,16 @@ const (
// [registry.IndexServer]: https://pkg.go.dev/github.com/docker/docker@v28.3.3+incompatible/registry#IndexServer
const authConfigKey = "https://index.docker.io/v1/"

var desktopAuthConfig = secretsengine.AuthConfig

func envAuthConfig(registryHostname string) (configtypes.AuthConfig, bool) {
if os.Getenv(configfile.DockerEnvConfigKey) == "" {
return configtypes.AuthConfig{}, false
}
authConfig, err := configfile.New("").GetAuthConfig(registryHostname)
return authConfig, err == nil && authConfig.Username != ""
}

// ResolveAuthConfig returns auth-config for the given registry from the
// credential-store. It returns an empty AuthConfig if no credentials were
// found.
Expand Down Expand Up @@ -199,9 +210,16 @@ func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (strin
if err != nil {
return "", err
}
authConfig, err := cfg.GetAuthConfig(reference.Domain(registryRef))
if err != nil {
return "", err
domain := reference.Domain(registryRef)
authConfig, ok := envAuthConfig(domain)
if !ok {
authConfig, ok = desktopAuthConfig(domain)
}
if !ok {
authConfig, err = cfg.GetAuthConfig(domain)
if err != nil {
return "", err
}
}

return authconfig.Encode(registrytypes.AuthConfig{
Expand Down
77 changes: 77 additions & 0 deletions cli/command/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,84 @@ func TestGetDefaultAuthConfig_HelperError(t *testing.T) {
assert.Check(t, is.ErrorContains(err, "docker-credential-fake-does-not-exist"))
}

func noDesktopSession(string) (configtypes.AuthConfig, bool) {
return configtypes.AuthConfig{}, false
}

func TestRetrieveAuthTokenFromImageDesktopSession(t *testing.T) {
const configFileContent = `{"auths": {
"https://index.docker.io/v1/": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="},
"ghcr.io": {"auth": "dXNlcm5hbWU6cGFzc3dvcmQ="}
}
}`
cfg := configfile.ConfigFile{}
assert.NilError(t, cfg.LoadFromReader(bytes.NewReader([]byte(configFileContent))))

var lookups []string
command.SetDesktopAuthConfig(t, func(registryHostname string) (configtypes.AuthConfig, bool) {
lookups = append(lookups, registryHostname)
if registryHostname != "docker.io" {
return configtypes.AuthConfig{}, false
}
return configtypes.AuthConfig{
Username: "hubuser",
Password: "desktop-token",
ServerAddress: "https://index.docker.io/v1/",
}, true
})

tests := []struct {
imageRef string
expected registry.AuthConfig
}{
{
imageRef: "ubuntu",
expected: registry.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: "https://index.docker.io/v1/"},
},
{
imageRef: "ghcr.io/foo/bar",
expected: registry.AuthConfig{Username: "username", Password: "password", ServerAddress: "ghcr.io"},
},
}
for _, tc := range tests {
t.Run(tc.imageRef, func(t *testing.T) {
actual, err := command.RetrieveAuthTokenFromImage(&cfg, tc.imageRef)
assert.NilError(t, err)
expected, err := authconfig.Encode(tc.expected)
assert.NilError(t, err)
assert.Equal(t, actual, expected)
})
}
assert.Check(t, is.DeepEqual(lookups, []string{"docker.io", "ghcr.io"}))
}

func TestRetrieveAuthTokenFromImageEnvTakesPrecedenceOverDesktop(t *testing.T) {
t.Setenv(configfile.DockerEnvConfigKey, `{"auths":{"https://index.docker.io/v1/":{"auth":"ZW52dXNlcjplbnZwYXNz"}}}`)

var lookups []string
command.SetDesktopAuthConfig(t, func(registryHostname string) (configtypes.AuthConfig, bool) {
lookups = append(lookups, registryHostname)
return configtypes.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: registryHostname}, true
})
cfg := configfile.ConfigFile{}

actual, err := command.RetrieveAuthTokenFromImage(&cfg, "ubuntu")
assert.NilError(t, err)
expected, err := authconfig.Encode(registry.AuthConfig{Username: "envuser", Password: "envpass", ServerAddress: "https://index.docker.io/v1/"})
assert.NilError(t, err)
assert.Check(t, is.Equal(actual, expected))
assert.Check(t, is.Len(lookups, 0), "Docker Desktop must not be asked for a registry in DOCKER_AUTH_CONFIG")

actual, err = command.RetrieveAuthTokenFromImage(&cfg, "dhi.io/python")
assert.NilError(t, err)
expected, err = authconfig.Encode(registry.AuthConfig{Username: "hubuser", Password: "desktop-token", ServerAddress: "dhi.io"})
assert.NilError(t, err)
assert.Check(t, is.Equal(actual, expected))
assert.Check(t, is.DeepEqual(lookups, []string{"dhi.io"}))
}

func TestRetrieveAuthTokenFromImage(t *testing.T) {
command.SetDesktopAuthConfig(t, noDesktopSession)
// configFileContent contains a plain-text "username:password", as stored by
// the plain-text store;
// https://github.com/docker/cli/blob/v28.0.4/cli/config/configfile/file.go#L218-L229
Expand Down
12 changes: 12 additions & 0 deletions docs/reference/commandline/login.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,18 @@ With Docker Desktop, the credential store is already installed and configured
for you. Unless you want to change the credential store used by Docker Desktop,
you can skip the following steps.

#### Docker Desktop sign-in

When Docker Desktop is running and you are signed in, commands that pull or
push images through the Docker Engine, such as `docker pull`, `docker push`,
`docker run`, and `docker service create`, use the Docker Desktop session for
Docker Hub (`docker.io`) and Docker Hardened Images (`dhi.io`). The session is
used before the credential store, credential helpers, and the configuration
file. Credentials set in the `DOCKER_AUTH_CONFIG` environment variable take
precedence over the Docker Desktop session. If Docker Desktop is not installed
or not running, or you are not signed in, the Docker CLI uses its own
credentials.

#### Configure the credential store

You need to specify the credential store in `$HOME/.docker/config.json`
Expand Down
Loading
Loading