Compare commits

...

22 Commits

Author SHA1 Message Date
Harald Albers a7bb71acbb
Merge e03af58525 into 0ab0eca8bd 2024-10-22 07:41:16 +00:00
Harald Albers e03af58525 Parametrized existing completion tests
Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-22 07:40:55 +00:00
Sebastiaan van Stijn 0ab0eca8bd
Merge pull request #5550 from thaJeztah/login_minor_refactor
cli/command: PromptUserForCredentials: assorted minor improvements and (linting) fixes
2024-10-21 23:23:06 +02:00
Harald Albers 3517b1bf87 Add tests for `events --filter daemon=`
Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-21 13:02:59 +00:00
Harald Albers 6baf2a0347 Add tests for `events --filter network=`
Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-19 22:48:52 +00:00
Harald Albers 3eaad535b3 Add tests for `events --filter container=`
Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-19 21:48:49 +00:00
Harald Albers 4f7abb8db6 Fix linter errors
Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-19 16:00:41 +00:00
Sebastiaan van Stijn 4b7a1e4613
cli/command: PromptUserForCredentials: suppress unhandled errors
Keep the linters (and my IDE) happy; these errors should be safe to ignore.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 13:24:19 +02:00
Sebastiaan van Stijn 378a3d7d36
cli/command: PromptUserForCredentials: use consts for all hints
This message resulted in code-lines that were too long; move it to a
const together with the other hint. While at it, also suppress unhandled
error, and touch-up the code-comment.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 13:23:29 +02:00
Sebastiaan van Stijn 3d8b49523d
cli/command: PromptUserForCredentials: print error on terminal restore fail
If restoring the terminal state fails, "echo" no longer works, which means
that anything the user types is no longer shown. The login itself may already
have succeeded, so we should not fail the command, but it's good to inform
the user that this happened, which may give them a clue why things no longer
work as they expect them to work.

With this patch:

    docker login -u yourname
    Password:
    Error: failed to restore terminal state to echo input: something bad happened

    Login Succeeded

We should consider printing instructions how  to restore this manually (other
than restarting the shell). e.g., 'run stty echo' when in a Linux or macOS shell,
but PowerShell and CMD.exe may need different instructions.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 12:49:44 +02:00
Sebastiaan van Stijn a21a5f4243
cli/command: PromptUserForCredentials: always trim password
we don't support empty passwords; when prompting the user for a password,
we already trim the result, but we didn't do the same for a password that's
passed through stdin or through the `-p` / `--password` flag.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 12:10:46 +02:00
Sebastiaan van Stijn eda78e9cdc
cli/command: PromptUserForCredentials: move trimming where it's used
- move trimming defaultUsername inside the if-branch, as it's the only
  location where the result of the trimmed username is use.
- do the reverse for trimming argUser, because the result of trimming
  argUser is used outside of the if-branch (not just for the condition).
  putting it inside the condition makes it easy to assume the result is
  only used locally.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 12:07:51 +02:00
Sebastiaan van Stijn 581cf36bd4
cli/command: PromptUserForCredentials: move "post" check for empty name
move the "post" check for username being empty inside the branch
that's handling the username, as it's the only branch where username
is mutated after checking if it's empty.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 12:06:49 +02:00
Sebastiaan van Stijn a55cfe5f82
cli/command: PromptUserForCredentials: inline isDefaultRegistry
remove isDefaultRegistry and inline it where it's used; the code-comment
already outlines what we're looking for, so the intermediate var didn't
add much currently.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 11:58:42 +02:00
Sebastiaan van Stijn 3a8485085d
cli/command: PromptUserForCredentials: remove named output variables
This function has multiple conditional branches, which makes it harder
to see at a glance whether authConfig may be partially populated. This
patch instead returns a fresh instance for error returns to prevent any
confusion.

It also removes the named output variables, as they're now no longer used,
and the returned types should already be descriptive enough to understand
what's returned.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-10-19 11:46:21 +02:00
Harald Albers 8ae6ad6256 Refactor API helper functions
Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-18 16:55:23 +00:00
Harald Albers c7af8da7b9 Add remaining completions for `events --filter`
Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-18 16:53:52 +00:00
Harald Albers c5306f3c1a Use existing constants in completion of types and events in `events --filter`
Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-18 14:34:19 +00:00
Harald Albers 3f3ff40ee4 Add error handling
Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-17 21:59:53 +00:00
Harald Albers c17b87a713 Revert "[WIP] Refactor network name completion"
This reverts commit a09c2047d4.

Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-17 21:59:52 +00:00
Harald Albers a09c2047d4 [WIP] Refactor network name completion
Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-17 15:41:38 +00:00
Harald Albers 616b7c974b [WIP] Completion for events --filter
Signed-off-by: Harald Albers <github@albersweb.de>
2024-10-17 15:41:38 +00:00
5 changed files with 414 additions and 28 deletions

View File

@ -18,20 +18,24 @@ import (
"github.com/pkg/errors"
)
const patSuggest = "You can log in with your password or a Personal Access " +
"Token (PAT). Using a limited-scope PAT grants better security and is required " +
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
const (
registerSuggest = "Log in with your Docker ID or email address to push and pull images from Docker Hub. " +
"If you don't have a Docker ID, head over to https://hub.docker.com/ to create one."
patSuggest = "You can log in with your password or a Personal Access " +
"Token (PAT). Using a limited-scope PAT grants better security and is required " +
"for organizations using SSO. Learn more at https://docs.docker.com/go/access-tokens/"
)
// RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
// for the given command.
func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) registrytypes.RequestAuthConfig {
return func(ctx context.Context) (string, error) {
fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName)
_, _ = fmt.Fprintf(cli.Out(), "\nLogin prior to %s:\n", cmdName)
indexServer := registry.GetAuthConfigKey(index)
isDefaultRegistry := indexServer == registry.IndexServer
authConfig, err := GetDefaultAuthConfig(cli.ConfigFile(), true, indexServer, isDefaultRegistry)
if err != nil {
fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
_, _ = fmt.Fprintf(cli.Err(), "Unable to retrieve stored credentials for %s, error: %s.\n", indexServer, err)
}
select {
@ -111,7 +115,7 @@ func ConfigureAuth(ctx context.Context, cli Cli, flUser, flPassword string, auth
// If defaultUsername is not empty, the username prompt includes that username
// and the user can hit enter without inputting a username to use that default
// username.
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (authConfig registrytypes.AuthConfig, err error) {
func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword, defaultUsername, serverAddress string) (registrytypes.AuthConfig, error) {
// On Windows, force the use of the regular OS stdin stream.
//
// See:
@ -124,57 +128,71 @@ func PromptUserForCredentials(ctx context.Context, cli Cli, argUser, argPassword
cli.SetIn(streams.NewIn(os.Stdin))
}
isDefaultRegistry := serverAddress == registry.IndexServer
defaultUsername = strings.TrimSpace(defaultUsername)
if argUser = strings.TrimSpace(argUser); argUser == "" {
if isDefaultRegistry {
// if this is a default registry (docker hub), then display the following message.
fmt.Fprintln(cli.Out(), "Log in with your Docker ID or email address to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com/ to create one.")
argUser = strings.TrimSpace(argUser)
if argUser == "" {
if serverAddress == registry.IndexServer {
// When signing in to the default (Docker Hub) registry, we display
// hints for creating an account, and (if hints are enabled), using
// a token instead of a password.
_, _ = fmt.Fprintln(cli.Out(), registerSuggest)
if hints.Enabled() {
fmt.Fprintln(cli.Out(), patSuggest)
fmt.Fprintln(cli.Out())
_, _ = fmt.Fprintln(cli.Out(), patSuggest)
_, _ = fmt.Fprintln(cli.Out())
}
}
var prompt string
defaultUsername = strings.TrimSpace(defaultUsername)
if defaultUsername == "" {
prompt = "Username: "
} else {
prompt = fmt.Sprintf("Username (%s): ", defaultUsername)
}
var err error
argUser, err = PromptForInput(ctx, cli.In(), cli.Out(), prompt)
if err != nil {
return authConfig, err
return registrytypes.AuthConfig{}, err
}
if argUser == "" {
argUser = defaultUsername
}
if argUser == "" {
return registrytypes.AuthConfig{}, errors.Errorf("Error: Non-null Username Required")
}
}
if argUser == "" {
return authConfig, errors.Errorf("Error: Non-null Username Required")
}
argPassword = strings.TrimSpace(argPassword)
if argPassword == "" {
restoreInput, err := DisableInputEcho(cli.In())
if err != nil {
return authConfig, err
return registrytypes.AuthConfig{}, err
}
defer restoreInput()
defer func() {
if err := restoreInput(); err != nil {
// TODO(thaJeztah): we should consider printing instructions how
// to restore this manually (other than restarting the shell).
// e.g., 'run stty echo' when in a Linux or macOS shell, but
// PowerShell and CMD.exe may need different instructions.
_, _ = fmt.Fprintln(cli.Err(), "Error: failed to restore terminal state to echo input:", err)
}
}()
argPassword, err = PromptForInput(ctx, cli.In(), cli.Out(), "Password: ")
if err != nil {
return authConfig, err
return registrytypes.AuthConfig{}, err
}
fmt.Fprint(cli.Out(), "\n")
_, _ = fmt.Fprintln(cli.Out())
if argPassword == "" {
return authConfig, errors.Errorf("Error: Password Required")
return registrytypes.AuthConfig{}, errors.Errorf("Error: Password Required")
}
}
authConfig.Username = argUser
authConfig.Password = argPassword
authConfig.ServerAddress = serverAddress
return authConfig, nil
return registrytypes.AuthConfig{
Username: argUser,
Password: argPassword,
ServerAddress: serverAddress,
}, nil
}
// RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete

View File

@ -3,6 +3,8 @@ package system
import (
"context"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/events"
@ -19,6 +21,9 @@ type fakeClient struct {
eventsFn func(context.Context, events.ListOptions) (<-chan events.Message, <-chan error)
containerPruneFunc func(ctx context.Context, pruneFilters filters.Args) (container.PruneReport, error)
networkPruneFunc func(ctx context.Context, pruneFilter filters.Args) (network.PruneReport, error)
containerListFunc func(context.Context, container.ListOptions) ([]container.Summary, error)
infoFunc func(ctx context.Context) (system.Info, error)
networkListFunc func(ctx context.Context, options network.ListOptions) ([]network.Summary, error)
}
func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) {
@ -46,3 +51,24 @@ func (cli *fakeClient) NetworksPrune(ctx context.Context, pruneFilter filters.Ar
}
return network.PruneReport{}, nil
}
func (cli *fakeClient) ContainerList(ctx context.Context, options container.ListOptions) ([]container.Summary, error) {
if cli.containerListFunc != nil {
return cli.containerListFunc(ctx, options)
}
return []container.Summary{}, nil
}
func (cli *fakeClient) Info(ctx context.Context) (system.Info, error) {
if cli.infoFunc != nil {
return cli.infoFunc(ctx)
}
return system.Info{}, nil
}
func (cli *fakeClient) NetworkList(ctx context.Context, options network.ListOptions) ([]network.Summary, error) {
if cli.networkListFunc != nil {
return cli.networkListFunc(ctx, options)
}
return []network.Summary{}, nil
}

View File

@ -0,0 +1,241 @@
package system
import (
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types/volume"
"github.com/docker/docker/api/types/events"
"github.com/docker/cli/cli/command/completion"
"github.com/spf13/cobra"
)
var (
eventFilters = []string{"container", "daemon", "event", "image", "label", "network", "node", "scope", "type", "volume"}
// eventTypes is a list of all event types.
// This should be moved to the moby codebase once its usage is consolidated here.
eventTypes = []events.Type{
events.BuilderEventType,
events.ConfigEventType,
events.ContainerEventType,
events.DaemonEventType,
events.ImageEventType,
events.NetworkEventType,
events.NodeEventType,
events.PluginEventType,
events.SecretEventType,
events.ServiceEventType,
events.VolumeEventType,
}
// eventActions is a list of all event actions.
// This should be moved to the moby codebase once its usage is consolidated here.
eventActions = []events.Action{
events.ActionCreate,
events.ActionStart,
events.ActionRestart,
events.ActionStop,
events.ActionCheckpoint,
events.ActionPause,
events.ActionUnPause,
events.ActionAttach,
events.ActionDetach,
events.ActionResize,
events.ActionUpdate,
events.ActionRename,
events.ActionKill,
events.ActionDie,
events.ActionOOM,
events.ActionDestroy,
events.ActionRemove,
events.ActionCommit,
events.ActionTop,
events.ActionCopy,
events.ActionArchivePath,
events.ActionExtractToDir,
events.ActionExport,
events.ActionImport,
events.ActionSave,
events.ActionLoad,
events.ActionTag,
events.ActionUnTag,
events.ActionPush,
events.ActionPull,
events.ActionPrune,
events.ActionDelete,
events.ActionEnable,
events.ActionDisable,
events.ActionConnect,
events.ActionDisconnect,
events.ActionReload,
events.ActionMount,
events.ActionUnmount,
events.ActionExecCreate,
events.ActionExecStart,
events.ActionExecDie,
events.ActionExecDetach,
events.ActionHealthStatus,
events.ActionHealthStatusRunning,
events.ActionHealthStatusHealthy,
events.ActionHealthStatusUnhealthy,
}
)
// completeFilters provides completion for the filters that can be used with `--filter`.
func completeFilters(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if strings.HasPrefix(toComplete, "container=") {
return prefixWith("container=", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoFileComp
}
if strings.HasPrefix(toComplete, "daemon=") {
return prefixWith("daemon=", daemonNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
}
if strings.HasPrefix(toComplete, "event=") {
return prefixWith("event=", validEventNames()), cobra.ShellCompDirectiveNoFileComp
}
if strings.HasPrefix(toComplete, "image=") {
return prefixWith("image=", imageNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
}
if strings.HasPrefix(toComplete, "label=") {
return nil, cobra.ShellCompDirectiveNoFileComp
}
if strings.HasPrefix(toComplete, "network=") {
return prefixWith("network=", networkNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
}
if strings.HasPrefix(toComplete, "node=") {
return prefixWith("node=", nodeNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
}
if strings.HasPrefix(toComplete, "scope=") {
return prefixWith("scope=", []string{"local", "swarm"}), cobra.ShellCompDirectiveNoFileComp
}
if strings.HasPrefix(toComplete, "type=") {
return prefixWith("type=", eventTypeNames()), cobra.ShellCompDirectiveNoFileComp
}
if strings.HasPrefix(toComplete, "volume=") {
return prefixWith("volume=", volumeNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
}
return postfixWith("=", eventFilters), cobra.ShellCompDirectiveNoSpace
}
}
func prefixWith(prefix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = prefix + v
}
return result
}
func postfixWith(postfix string, values []string) []string {
result := make([]string, len(values))
for i, v := range values {
result[i] = v + postfix
}
return result
}
// eventTypeNames provides a list of all event types.
// The list is derived from eventTypes.
func eventTypeNames() []string {
names := make([]string, len(eventTypes))
for i, eventType := range eventTypes {
names[i] = string(eventType)
}
return names
}
// validEventNames provides a list of all event actions.
// The list is derived from eventActions.
// Actions that are not suitable for usage in completions are removed.
func validEventNames() []string {
names := []string{}
for _, eventAction := range eventActions {
if strings.Contains(string(eventAction), " ") {
continue
}
names = append(names, string(eventAction))
}
return names
}
// containerNames contacts the API to get names and optionally IDs of containers.
// In case of an error, an empty list is returned.
func containerNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command, args []string, toComplete string) []string {
names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete)
if names == nil {
return []string{}
}
return names
}
// daemonNames contacts the API to get name and ID of the current docker daemon.
// In case of an error, an empty list is returned.
func daemonNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
info, err := dockerCLI.Client().Info(cmd.Context())
if err != nil {
return []string{}
}
return []string{info.Name, info.ID}
}
// imageNames contacts the API to get a list of image names.
// In case of an error, an empty list is returned.
func imageNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
list, err := dockerCLI.Client().ImageList(cmd.Context(), image.ListOptions{})
if err != nil {
return []string{}
}
names := []string{}
for _, img := range list {
names = append(names, img.RepoTags...)
}
return names
}
// networkNames contacts the API to get a list of network names.
// In case of an error, an empty list is returned.
func networkNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
list, err := dockerCLI.Client().NetworkList(cmd.Context(), network.ListOptions{})
if err != nil {
return []string{}
}
names := []string{}
for _, nw := range list {
names = append(names, nw.Name)
}
return names
}
// nodeNames contacts the API to get a list of node names.
// In case of an error, an empty list is returned.
func nodeNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
list, err := dockerCLI.Client().NodeList(cmd.Context(), types.NodeListOptions{})
if err != nil {
return []string{}
}
names := []string{}
for _, node := range list {
names = append(names, node.Description.Hostname)
}
return names
}
// volumeNames contacts the API to get a list of volume names.
// In case of an error, an empty list is returned.
func volumeNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
list, err := dockerCLI.Client().VolumeList(cmd.Context(), volume.ListOptions{})
if err != nil {
return []string{}
}
names := []string{}
for _, v := range list.Volumes {
names = append(names, v.Name)
}
return names
}

View File

@ -0,0 +1,99 @@
package system
import (
"context"
"errors"
"fmt"
"testing"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/api/types/network"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/builders"
"github.com/docker/docker/api/types/container"
"github.com/spf13/cobra"
"gotest.tools/v3/assert"
)
func TestCompleteEventFilter(t *testing.T) {
tests := []struct {
client *fakeClient
toComplete string
expected []string
}{
{
client: &fakeClient{
containerListFunc: func(_ context.Context, _ container.ListOptions) ([]container.Summary, error) {
return []container.Summary{
*builders.Container("c1"),
*builders.Container("c2"),
}, nil
},
},
toComplete: "container=",
expected: []string{"container=c1", "container=c2"},
},
{
client: &fakeClient{
containerListFunc: func(_ context.Context, _ container.ListOptions) ([]container.Summary, error) {
return nil, errors.New("API error")
},
},
toComplete: "container=",
expected: []string{},
},
{
client: &fakeClient{
infoFunc: func(ctx context.Context) (system.Info, error) {
return system.Info{
ID: "daemon-id",
Name: "daemon-name",
}, nil
},
},
toComplete: "daemon=",
expected: []string{"daemon=daemon-name", "daemon=daemon-id"},
},
{
client: &fakeClient{
infoFunc: func(ctx context.Context) (system.Info, error) {
return system.Info{}, errors.New("API error")
},
},
toComplete: "daemon=",
expected: []string{},
},
{
client: &fakeClient{
networkListFunc: func(_ context.Context, _ network.ListOptions) ([]network.Summary, error) {
return []network.Summary{
*builders.NetworkResource(builders.NetworkResourceName("nw1")),
*builders.NetworkResource(builders.NetworkResourceName("nw2")),
}, nil
},
},
toComplete: "network=",
expected: []string{"network=nw1", "network=nw2"},
},
{
client: &fakeClient{
networkListFunc: func(_ context.Context, _ network.ListOptions) ([]network.Summary, error) {
return nil, errors.New("API error")
},
},
toComplete: "network=",
expected: []string{},
},
}
for _, tc := range tests {
cli := test.NewFakeCli(tc.client)
completions, directive := completeFilters(cli)(NewEventsCommand(cli), nil, tc.toComplete)
assert.DeepEqual(t, completions, tc.expected)
assert.Equal(t, directive, cobra.ShellCompDirectiveNoFileComp, fmt.Sprintf("wrong directive in completion for '%s'", tc.toComplete))
}
}

View File

@ -50,6 +50,8 @@ func NewEventsCommand(dockerCli command.Cli) *cobra.Command {
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
flags.StringVar(&options.format, "format", "", flagsHelper.InspectFormatHelp) // using the same flag description as "inspect" commands for now.
_ = cmd.RegisterFlagCompletionFunc("filter", completeFilters(dockerCli))
return cmd
}