From 0b151a80ded269a752cdfda4b4bb9c59004ab93f Mon Sep 17 00:00:00 2001 From: Michael Zampani Date: Mon, 2 Feb 2026 09:23:58 -0800 Subject: [PATCH] cli/command/registry: add whoami command Add a new 'docker whoami' command that displays the username of the currently logged-in user for Docker registries. The command follows existing patterns from login/logout commands. Features: - Display username for Docker Hub by default - Support specifying a custom registry as an argument - Add --all flag to list all authenticated registries with usernames - Proper error handling for non-authenticated registries - Comprehensive test coverage with 13 test cases Co-Authored-By: Claude Sonnet 4.5 --- cli/command/registry/whoami.go | 104 +++++++++ cli/command/registry/whoami_test.go | 319 +++++++++++++++++++++++++++ docs/reference/commandline/docker.md | 1 + docs/reference/commandline/whoami.md | 15 ++ 4 files changed, 439 insertions(+) create mode 100644 cli/command/registry/whoami.go create mode 100644 cli/command/registry/whoami_test.go create mode 100644 docs/reference/commandline/whoami.md diff --git a/cli/command/registry/whoami.go b/cli/command/registry/whoami.go new file mode 100644 index 000000000000..88d6e0766f64 --- /dev/null +++ b/cli/command/registry/whoami.go @@ -0,0 +1,104 @@ +package registry + +import ( + "context" + "errors" + "fmt" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config/credentials" + "github.com/docker/cli/internal/commands" + "github.com/docker/cli/internal/registry" + "github.com/spf13/cobra" +) + +func init() { + commands.Register(newWhoamiCommand) +} + +type whoamiOptions struct { + serverAddress string + all bool +} + +// newWhoamiCommand creates a new `docker whoami` command +func newWhoamiCommand(dockerCLI command.Cli) *cobra.Command { + var opts whoamiOptions + + cmd := &cobra.Command{ + Use: "whoami [SERVER]", + Short: "Display the username of the currently logged in user", + Long: "Display the username of the currently logged in user.\nDefaults to Docker Hub if no server is specified.", + Args: cli.RequiresMaxArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + opts.serverAddress = args[0] + } + return runWhoami(cmd.Context(), dockerCLI, opts) + }, + Annotations: map[string]string{ + "category-top": "10", + }, + ValidArgsFunction: cobra.NoFileCompletions, + DisableFlagsInUseLine: true, + } + + flags := cmd.Flags() + flags.BoolVar(&opts.all, "all", false, "Display usernames for all authenticated registries") + + return cmd +} + +func runWhoami(_ context.Context, dockerCLI command.Cli, opts whoamiOptions) error { + maybePrintEnvAuthWarning(dockerCLI) + + if opts.all { + return runWhoamiAll(dockerCLI) + } + return runWhoamiSingle(dockerCLI, opts) +} + +func runWhoamiSingle(dockerCLI command.Cli, opts whoamiOptions) error { + serverAddress := opts.serverAddress + if serverAddress == "" || serverAddress == registry.DefaultNamespace { + serverAddress = registry.IndexServer + } else { + serverAddress = credentials.ConvertToHostname(serverAddress) + } + + authConfig, err := dockerCLI.ConfigFile().GetAuthConfig(serverAddress) + if err != nil { + return err + } + + if authConfig.Username == "" { + registryName := "Docker Hub" + if opts.serverAddress != "" && opts.serverAddress != registry.DefaultNamespace { + registryName = serverAddress + } + return fmt.Errorf("not logged in to %s", registryName) + } + + fmt.Fprintln(dockerCLI.Out(), authConfig.Username) + return nil +} + +func runWhoamiAll(dockerCLI command.Cli) error { + creds, err := dockerCLI.ConfigFile().GetAllCredentials() + if err != nil { + return err + } + + if len(creds) == 0 { + return errors.New("not logged in to any registries") + } + + for serverAddress, authConfig := range creds { + if authConfig.Username != "" { + fmt.Fprintf(dockerCLI.Out(), "%s: %s\n", serverAddress, authConfig.Username) + } + } + + return nil +} diff --git a/cli/command/registry/whoami_test.go b/cli/command/registry/whoami_test.go new file mode 100644 index 000000000000..cdb242d72f98 --- /dev/null +++ b/cli/command/registry/whoami_test.go @@ -0,0 +1,319 @@ +package registry + +import ( + "context" + "testing" + + configtypes "github.com/docker/cli/cli/config/types" + "github.com/docker/cli/internal/registry" + "github.com/docker/cli/internal/test" + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" + "gotest.tools/v3/fs" +) + +func TestWhoamiNotLoggedIn(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-not-logged-in") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + cli.ConfigFile().Filename = tmpFile.Path() + + err := runWhoami(context.Background(), cli, whoamiOptions{}) + assert.Error(t, err, "not logged in to Docker Hub") + assert.Check(t, is.Equal("", cli.OutBuffer().String())) +} + +func TestWhoamiLoggedInDockerHub(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-docker-hub") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + + assert.NilError(t, configfile.GetCredentialsStore(registry.IndexServer).Store(configtypes.AuthConfig{ + Username: "testuser", + Password: "testpass", + ServerAddress: registry.IndexServer, + })) + + err := runWhoami(context.Background(), cli, whoamiOptions{}) + assert.NilError(t, err) + assert.Check(t, is.Equal("testuser\n", cli.OutBuffer().String())) +} + +func TestWhoamiLoggedInCustomRegistry(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-custom-registry") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + + customRegistry := "custom.registry.com" + assert.NilError(t, configfile.GetCredentialsStore(customRegistry).Store(configtypes.AuthConfig{ + Username: "customuser", + Password: "custompass", + ServerAddress: customRegistry, + })) + + err := runWhoami(context.Background(), cli, whoamiOptions{ + serverAddress: customRegistry, + }) + assert.NilError(t, err) + assert.Check(t, is.Equal("customuser\n", cli.OutBuffer().String())) +} + +func TestWhoamiNotLoggedInCustomRegistry(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-not-logged-in-custom") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + cli.ConfigFile().Filename = tmpFile.Path() + + customRegistry := "custom.registry.com" + err := runWhoami(context.Background(), cli, whoamiOptions{ + serverAddress: customRegistry, + }) + assert.Error(t, err, "not logged in to "+customRegistry) +} + +func TestWhoamiAll(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-all") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + + assert.NilError(t, configfile.GetCredentialsStore(registry.IndexServer).Store(configtypes.AuthConfig{ + Username: "hubuser", + Password: "hubpass", + ServerAddress: registry.IndexServer, + })) + + assert.NilError(t, configfile.GetCredentialsStore("custom1.registry.com").Store(configtypes.AuthConfig{ + Username: "custom1user", + Password: "custom1pass", + ServerAddress: "custom1.registry.com", + })) + + assert.NilError(t, configfile.GetCredentialsStore("custom2.registry.com").Store(configtypes.AuthConfig{ + Username: "custom2user", + Password: "custom2pass", + ServerAddress: "custom2.registry.com", + })) + + err := runWhoami(context.Background(), cli, whoamiOptions{ + all: true, + }) + assert.NilError(t, err) + + output := cli.OutBuffer().String() + assert.Check(t, is.Contains(output, "custom1.registry.com: custom1user")) + assert.Check(t, is.Contains(output, "custom2.registry.com: custom2user")) + assert.Check(t, is.Contains(output, registry.IndexServer+": hubuser")) +} + +func TestWhoamiAllNotLoggedIn(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-all-not-logged-in") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + cli.ConfigFile().Filename = tmpFile.Path() + + err := runWhoami(context.Background(), cli, whoamiOptions{ + all: true, + }) + assert.Error(t, err, "not logged in to any registries") +} + +func TestWhoamiWithDockerAuthConfig(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-docker-auth-config") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + + // Store credentials normally + assert.NilError(t, configfile.GetCredentialsStore(registry.IndexServer).Store(configtypes.AuthConfig{ + Username: "testuser", + Password: "testpass", + ServerAddress: registry.IndexServer, + })) + + // Set DOCKER_AUTH_CONFIG environment variable to trigger warning + t.Setenv("DOCKER_AUTH_CONFIG", `{"auths":{}}`) + + err := runWhoami(context.Background(), cli, whoamiOptions{}) + assert.NilError(t, err) + + // Should print warning about DOCKER_AUTH_CONFIG + assert.Check(t, is.Contains(cli.ErrBuffer().String(), "DOCKER_AUTH_CONFIG")) + assert.Check(t, is.Equal("testuser\n", cli.OutBuffer().String())) +} + +func TestWhoamiRegistryWithProtocol(t *testing.T) { + testCases := []struct { + name string + registry string + }{ + { + name: "with https protocol", + registry: "https://custom.registry.com", + }, + { + name: "with http protocol", + registry: "http://custom.registry.com", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-with-protocol") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + + // Store with normalized hostname (without protocol) + assert.NilError(t, configfile.GetCredentialsStore("custom.registry.com").Store(configtypes.AuthConfig{ + Username: "protocoluser", + Password: "protocolpass", + ServerAddress: "custom.registry.com", + })) + + // Query with protocol - should still work + err := runWhoami(context.Background(), cli, whoamiOptions{ + serverAddress: tc.registry, + }) + assert.NilError(t, err) + assert.Check(t, is.Equal("protocoluser\n", cli.OutBuffer().String())) + }) + } +} + +func TestWhoamiDockerIOAlias(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-docker-io-alias") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + + // Store with Docker Hub + assert.NilError(t, configfile.GetCredentialsStore(registry.IndexServer).Store(configtypes.AuthConfig{ + Username: "dockeriouser", + Password: "dockeriopass", + ServerAddress: registry.IndexServer, + })) + + // Query with docker.io should map to Docker Hub + err := runWhoami(context.Background(), cli, whoamiOptions{ + serverAddress: "docker.io", + }) + assert.NilError(t, err) + assert.Check(t, is.Equal("dockeriouser\n", cli.OutBuffer().String())) +} + +func TestWhoamiEmptyUsername(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-empty-username") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + + // Store credentials with empty username (token-based auth) + assert.NilError(t, configfile.GetCredentialsStore(registry.IndexServer).Store(configtypes.AuthConfig{ + Username: "", + IdentityToken: "sometoken", + ServerAddress: registry.IndexServer, + })) + + err := runWhoami(context.Background(), cli, whoamiOptions{}) + assert.Error(t, err, "not logged in to Docker Hub") +} + +func TestWhoamiAllSkipsEmptyUsernames(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-all-skip-empty") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + + // Store one with username + assert.NilError(t, configfile.GetCredentialsStore("custom.registry.com").Store(configtypes.AuthConfig{ + Username: "customuser", + Password: "custompass", + ServerAddress: "custom.registry.com", + })) + + // Store one without username (token-based) + assert.NilError(t, configfile.GetCredentialsStore("token.registry.com").Store(configtypes.AuthConfig{ + Username: "", + IdentityToken: "sometoken", + ServerAddress: "token.registry.com", + })) + + err := runWhoami(context.Background(), cli, whoamiOptions{ + all: true, + }) + assert.NilError(t, err) + + output := cli.OutBuffer().String() + // Should include the one with username + assert.Check(t, is.Contains(output, "custom.registry.com: customuser")) + // Should not include the one without username + assert.Assert(t, !is.Contains(output, "token.registry.com")().Success()) +} + +func TestWhoamiWithRegistryPort(t *testing.T) { + tmpFile := fs.NewFile(t, "test-whoami-with-port") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + + registryWithPort := "custom.registry.com:5000" + assert.NilError(t, configfile.GetCredentialsStore(registryWithPort).Store(configtypes.AuthConfig{ + Username: "portuser", + Password: "portpass", + ServerAddress: registryWithPort, + })) + + err := runWhoami(context.Background(), cli, whoamiOptions{ + serverAddress: registryWithPort, + }) + assert.NilError(t, err) + assert.Check(t, is.Equal("portuser\n", cli.OutBuffer().String())) +} + +func TestWhoamiClearsEnvironmentVariable(t *testing.T) { + // Test should not be affected by environment variable + t.Setenv("DOCKER_AUTH_CONFIG", "") + + tmpFile := fs.NewFile(t, "test-whoami-no-env") + defer tmpFile.Remove() + + cli := test.NewFakeCli(&fakeClient{}) + configfile := cli.ConfigFile() + configfile.Filename = tmpFile.Path() + + assert.NilError(t, configfile.GetCredentialsStore(registry.IndexServer).Store(configtypes.AuthConfig{ + Username: "fileuser", + Password: "filepass", + ServerAddress: registry.IndexServer, + })) + + err := runWhoami(context.Background(), cli, whoamiOptions{}) + assert.NilError(t, err) + assert.Check(t, is.Equal("fileuser\n", cli.OutBuffer().String())) + // Should not print warning when env var is not set + assert.Check(t, is.Equal("", cli.ErrBuffer().String())) +} diff --git a/docs/reference/commandline/docker.md b/docs/reference/commandline/docker.md index b926315defde..d43685553bc6 100644 --- a/docs/reference/commandline/docker.md +++ b/docs/reference/commandline/docker.md @@ -64,6 +64,7 @@ The base command for the Docker CLI. | [`version`](version.md) | Show the Docker version information | | [`volume`](volume.md) | Manage volumes | | [`wait`](wait.md) | Block until one or more containers stop, then print their exit codes | +| [`whoami`](whoami.md) | Display the username of the currently logged in user | ### Options diff --git a/docs/reference/commandline/whoami.md b/docs/reference/commandline/whoami.md new file mode 100644 index 000000000000..68a833ecaa4f --- /dev/null +++ b/docs/reference/commandline/whoami.md @@ -0,0 +1,15 @@ +# docker whoami + + +Display the username of the currently logged in user. +Defaults to Docker Hub if no server is specified. + +### Options + +| Name | Type | Default | Description | +|:--------|:-------|:--------|:---------------------------------------------------| +| `--all` | `bool` | | Display usernames for all authenticated registries | + + + +