From cc6c49de962b5edbd71c39755be6fb25f0793c6a Mon Sep 17 00:00:00 2001 From: c879873067877881111 Date: Tue, 18 Aug 2026 17:44:11 +0800 Subject: [PATCH 1/2] cli/command/registry: fix "docker logout docker.io" leaving credentials behind "docker login docker.io" resolves the default registry to registry.IndexServer ("https://index.docker.io/v1/") and stores the credentials under that key. "docker logout docker.io" only treated an empty server address as the default registry, so it looked for "docker.io", "http://docker.io" and "https://docker.io" instead. None of those exist, and fileStore.Erase returns nil for a key that is not present, so every removal "succeeded": the credentials stayed in config.json, "Removing login credentials for docker.io" was printed, and the command exited 0. Look for the full index address as well when logging out of the default registry by its namespace ("docker.io") or index hostname ("index.docker.io"). The existing lookups are kept, so credentials stored under a legacy key are still removed. Add tests covering the login/logout round-trip; logout had no test coverage. Signed-off-by: c879873067877881111 --- cli/command/registry/logout.go | 5 +++ cli/command/registry/logout_test.go | 49 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 cli/command/registry/logout_test.go diff --git a/cli/command/registry/logout.go b/cli/command/registry/logout.go index 16218f67d9a7..d69d32a957e4 100644 --- a/cli/command/registry/logout.go +++ b/cli/command/registry/logout.go @@ -60,6 +60,11 @@ func runLogout(ctx context.Context, dockerCLI command.Cli, serverAddress string) // the tries below are kept for backward compatibility where a user could have // saved the registry in one of the following format. regsToLogout = append(regsToLogout, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress) + // "docker login docker.io" stores credentials under the full index + // address, so look for that key as well. + if hostnameAddress == registry.DefaultNamespace || hostnameAddress == registry.IndexHostname { + regsToLogout = append(regsToLogout, registry.IndexServer) + } } if isDefaultRegistry { diff --git a/cli/command/registry/logout_test.go b/cli/command/registry/logout_test.go new file mode 100644 index 000000000000..2cb918abcc88 --- /dev/null +++ b/cli/command/registry/logout_test.go @@ -0,0 +1,49 @@ +package registry + +import ( + "context" + "path/filepath" + "testing" + + "github.com/docker/cli/cli/config/configfile" + "github.com/docker/cli/internal/test" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +// TestLogoutRemovesCredentialsStoredByLogin verifies that logging out with the +// same argument that was used to log in removes the stored credentials. +// +// "docker.io" is normalized to [registry.IndexServer] when logging in, so +// logout must apply the same normalization to find the entry again. +func TestLogoutRemovesCredentialsStoredByLogin(t *testing.T) { + for _, serverAddress := range []string{ + "", + "docker.io", + "index.docker.io", + "https://index.docker.io/v1/", + "myreg.example.com", + } { + name := serverAddress + if name == "" { + name = "no server address" + } + t.Run(name, func(t *testing.T) { + configFile := configfile.New(filepath.Join(t.TempDir(), "config.json")) + cli := test.NewFakeCli(&fakeClient{}) + cli.SetConfigFile(configFile) + + err := runLogin(context.Background(), cli, loginOptions{ + serverAddress: serverAddress, + user: "my-username", + password: "my-password", + }) + assert.NilError(t, err) + assert.Assert(t, is.Len(configFile.AuthConfigs, 1), "login did not store credentials") + + err = runLogout(context.Background(), cli, serverAddress) + assert.NilError(t, err) + assert.Check(t, is.Len(configFile.AuthConfigs, 0)) + }) + } +} From c8aacf00ed8bcc43e231945dc2723d8495ed2463 Mon Sep 17 00:00:00 2001 From: c879873067877881111 Date: Tue, 18 Aug 2026 17:51:28 +0800 Subject: [PATCH 2/2] cli/command/registry: clarify why these two hostnames Point at getAuthConfigKey in cli/config/configfile as the source of the "docker.io" / "index.docker.io" pairing, so the condition does not read as an ad-hoc choice. Signed-off-by: c879873067877881111 --- cli/command/registry/logout.go | 6 ++++-- cli/command/registry/logout_test.go | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cli/command/registry/logout.go b/cli/command/registry/logout.go index d69d32a957e4..8ed29e2c7324 100644 --- a/cli/command/registry/logout.go +++ b/cli/command/registry/logout.go @@ -60,8 +60,10 @@ func runLogout(ctx context.Context, dockerCLI command.Cli, serverAddress string) // the tries below are kept for backward compatibility where a user could have // saved the registry in one of the following format. regsToLogout = append(regsToLogout, hostnameAddress, "http://"+hostnameAddress, "https://"+hostnameAddress) - // "docker login docker.io" stores credentials under the full index - // address, so look for that key as well. + // Credentials for the default registry are stored under the full index + // address, which is the key "docker login docker.io" writes to. The + // hostnames below are the ones that getAuthConfigKey in + // cli/config/configfile maps to that key. if hostnameAddress == registry.DefaultNamespace || hostnameAddress == registry.IndexHostname { regsToLogout = append(regsToLogout, registry.IndexServer) } diff --git a/cli/command/registry/logout_test.go b/cli/command/registry/logout_test.go index 2cb918abcc88..e7212d09d6d4 100644 --- a/cli/command/registry/logout_test.go +++ b/cli/command/registry/logout_test.go @@ -14,8 +14,9 @@ import ( // TestLogoutRemovesCredentialsStoredByLogin verifies that logging out with the // same argument that was used to log in removes the stored credentials. // -// "docker.io" is normalized to [registry.IndexServer] when logging in, so -// logout must apply the same normalization to find the entry again. +// Credentials for the default registry are stored under +// [registry.IndexServer] regardless of the spelling passed to "docker login", +// so logout has to look for that key as well to find them again. func TestLogoutRemovesCredentialsStoredByLogin(t *testing.T) { for _, serverAddress := range []string{ "",