diff --git a/cli/command/node/completion.go b/cli/command/node/completion.go new file mode 100644 index 0000000000..3b07b801e7 --- /dev/null +++ b/cli/command/node/completion.go @@ -0,0 +1,37 @@ +package node + +import ( + "os" + + "github.com/docker/cli/cli/command/completion" + "github.com/docker/docker/api/types" + "github.com/spf13/cobra" +) + +// completeNodeNames offers completion for swarm node (host)names and optional IDs. +// By default, only names are returned. +// Set DOCKER_COMPLETION_SHOW_NODE_IDS=yes to also complete IDs. +// +// TODO(thaJeztah): add support for filters. +func completeNodeNames(dockerCLI completion.APIClientProvider) completion.ValidArgsFn { + // https://github.com/docker/cli/blob/f9ced58158d5e0b358052432244b483774a1983d/contrib/completion/bash/docker#L41-L43 + showIDs := os.Getenv("DOCKER_COMPLETION_SHOW_NODE_IDS") == "yes" + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + list, err := dockerCLI.Client().NodeList(cmd.Context(), types.NodeListOptions{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + names := make([]string, 0, len(list)+1) + for _, node := range list { + if showIDs { + names = append(names, node.Description.Hostname, node.ID) + } else { + names = append(names, node.Description.Hostname) + } + } + // Nodes allow "self" as magic word for the current node. + names = append(names, "self") + return names, cobra.ShellCompDirectiveNoFileComp + } +} diff --git a/cli/command/node/demote.go b/cli/command/node/demote.go index 9f6b25ac96..5ede173cd1 100644 --- a/cli/command/node/demote.go +++ b/cli/command/node/demote.go @@ -18,6 +18,7 @@ func newDemoteCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runDemote(cmd.Context(), dockerCli, args) }, + ValidArgsFunction: completeNodeNames(dockerCli), } } diff --git a/cli/command/node/inspect.go b/cli/command/node/inspect.go index 270a14bd2b..2feb8dc138 100644 --- a/cli/command/node/inspect.go +++ b/cli/command/node/inspect.go @@ -32,6 +32,7 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { opts.nodeIds = args return runInspect(cmd.Context(), dockerCli, opts) }, + ValidArgsFunction: completeNodeNames(dockerCli), } flags := cmd.Flags() diff --git a/cli/command/node/list.go b/cli/command/node/list.go index f64b8174ff..51b3f4afe1 100644 --- a/cli/command/node/list.go +++ b/cli/command/node/list.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/api/types/system" "github.com/fvbommel/sortorder" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) type listOptions struct { @@ -40,6 +41,12 @@ func newListCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp) flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) return cmd } diff --git a/cli/command/node/promote.go b/cli/command/node/promote.go index 2257273972..9832295264 100644 --- a/cli/command/node/promote.go +++ b/cli/command/node/promote.go @@ -18,6 +18,7 @@ func newPromoteCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runPromote(cmd.Context(), dockerCli, args) }, + ValidArgsFunction: completeNodeNames(dockerCli), } } diff --git a/cli/command/node/ps.go b/cli/command/node/ps.go index 207ecff575..3174c6d5e6 100644 --- a/cli/command/node/ps.go +++ b/cli/command/node/ps.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) type psOptions struct { @@ -41,7 +42,7 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { return runPs(cmd.Context(), dockerCli, options) }, - ValidArgsFunction: completion.NoComplete, + ValidArgsFunction: completeNodeNames(dockerCli), } flags := cmd.Flags() flags.BoolVar(&options.noTrunc, "no-trunc", false, "Do not truncate output") @@ -50,6 +51,12 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template") flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display task IDs") + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) return cmd } diff --git a/cli/command/node/remove.go b/cli/command/node/remove.go index 8e9460c95a..6ded0add42 100644 --- a/cli/command/node/remove.go +++ b/cli/command/node/remove.go @@ -26,6 +26,7 @@ func newRemoveCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runRemove(cmd.Context(), dockerCli, args, opts) }, + ValidArgsFunction: completeNodeNames(dockerCli), } flags := cmd.Flags() flags.BoolVarP(&opts.force, "force", "f", false, "Force remove a node from the swarm") diff --git a/cli/command/node/update.go b/cli/command/node/update.go index 2083aeaf5d..a225dbb1b0 100644 --- a/cli/command/node/update.go +++ b/cli/command/node/update.go @@ -6,6 +6,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" "github.com/docker/docker/api/types/swarm" "github.com/pkg/errors" @@ -25,6 +26,7 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runUpdate(cmd.Context(), dockerCli, cmd.Flags(), args[0]) }, + ValidArgsFunction: completeNodeNames(dockerCli), } flags := cmd.Flags() @@ -33,6 +35,15 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { flags.Var(&options.annotations.labels, flagLabelAdd, `Add or update a node label ("key=value")`) labelKeys := opts.NewListOpts(nil) flags.Var(&labelKeys, flagLabelRemove, "Remove a node label if exists") + + _ = cmd.RegisterFlagCompletionFunc(flagRole, completion.FromList("worker", "manager")) + _ = cmd.RegisterFlagCompletionFunc(flagAvailability, completion.FromList("active", "pause", "drain")) + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) return cmd } diff --git a/cli/command/service/cmd.go b/cli/command/service/cmd.go index aa54d54e11..66214d99c2 100644 --- a/cli/command/service/cmd.go +++ b/cli/command/service/cmd.go @@ -1,6 +1,8 @@ package service import ( + "os" + "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/completion" @@ -34,16 +36,25 @@ func NewServiceCommand(dockerCli command.Cli) *cobra.Command { return cmd } -// CompletionFn offers completion for swarm services +// CompletionFn offers completion for swarm service names and optional IDs. +// By default, only names are returned. +// Set DOCKER_COMPLETION_SHOW_SERVICE_IDS=yes to also complete IDs. func CompletionFn(dockerCLI completion.APIClientProvider) completion.ValidArgsFn { + // https://github.com/docker/cli/blob/f9ced58158d5e0b358052432244b483774a1983d/contrib/completion/bash/docker#L41-L43 + showIDs := os.Getenv("DOCKER_COMPLETION_SHOW_SERVICE_IDS") == "yes" return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { list, err := dockerCLI.Client().ServiceList(cmd.Context(), types.ServiceListOptions{}) if err != nil { return nil, cobra.ShellCompDirectiveError } - var names []string + + names := make([]string, 0, len(list)) for _, service := range list { - names = append(names, service.ID) + if showIDs { + names = append(names, service.Spec.Name, service.ID) + } else { + names = append(names, service.Spec.Name) + } } return names, cobra.ShellCompDirectiveNoFileComp } diff --git a/cli/command/service/create.go b/cli/command/service/create.go index ec3cf11953..60e2b09768 100644 --- a/cli/command/service/create.go +++ b/cli/command/service/create.go @@ -16,7 +16,7 @@ import ( "github.com/spf13/pflag" ) -func newCreateCommand(dockerCli command.Cli) *cobra.Command { +func newCreateCommand(dockerCLI command.Cli) *cobra.Command { opts := newServiceOptions() cmd := &cobra.Command{ @@ -28,7 +28,7 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { if len(args) > 1 { opts.args = args[1:] } - return runCreate(cmd.Context(), dockerCli, cmd.Flags(), opts) + return runCreate(cmd.Context(), dockerCLI, cmd.Flags(), opts) }, ValidArgsFunction: completion.NoComplete, } @@ -75,6 +75,28 @@ func newCreateCommand(dockerCli command.Cli) *cobra.Command { flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"}) flags.SetInterspersed(false) + + // TODO(thaJeztah): add completion for capabilities, stop-signal (currently non-exported in container package) + // _ = cmd.RegisterFlagCompletionFunc(flagCapAdd, completeLinuxCapabilityNames) + // _ = cmd.RegisterFlagCompletionFunc(flagCapDrop, completeLinuxCapabilityNames) + // _ = cmd.RegisterFlagCompletionFunc(flagStopSignal, completeSignals) + + _ = cmd.RegisterFlagCompletionFunc(flagMode, completion.FromList("replicated", "global", "replicated-job", "global-job")) + _ = cmd.RegisterFlagCompletionFunc(flagEnv, completion.EnvVarNames) // TODO(thaJeztah): flagEnvRemove (needs to read current env-vars on the service) + _ = cmd.RegisterFlagCompletionFunc(flagEnvFile, completion.FileNames) + _ = cmd.RegisterFlagCompletionFunc(flagNetwork, completion.NetworkNames(dockerCLI)) + _ = cmd.RegisterFlagCompletionFunc(flagRestartCondition, completion.FromList("none", "on-failure", "any")) + _ = cmd.RegisterFlagCompletionFunc(flagRollbackOrder, completion.FromList("start-first", "stop-first")) + _ = cmd.RegisterFlagCompletionFunc(flagRollbackFailureAction, completion.FromList("pause", "continue")) + _ = cmd.RegisterFlagCompletionFunc(flagUpdateOrder, completion.FromList("start-first", "stop-first")) + _ = cmd.RegisterFlagCompletionFunc(flagUpdateFailureAction, completion.FromList("pause", "continue", "rollback")) + + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) return cmd } diff --git a/cli/command/service/inspect.go b/cli/command/service/inspect.go index 28b957f773..addd14bca2 100644 --- a/cli/command/service/inspect.go +++ b/cli/command/service/inspect.go @@ -9,6 +9,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/formatter" flagsHelper "github.com/docker/cli/cli/flags" "github.com/docker/docker/api/types" @@ -16,6 +17,7 @@ import ( "github.com/docker/docker/errdefs" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) type inspectOptions struct { @@ -47,6 +49,13 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.format, "format", "f", "", flagsHelper.InspectFormatHelp) flags.BoolVar(&opts.pretty, "pretty", false, "Print the information in a human friendly format") + + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) return cmd } diff --git a/cli/command/service/list.go b/cli/command/service/list.go index b67b0d130d..03251eb25f 100644 --- a/cli/command/service/list.go +++ b/cli/command/service/list.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) type listOptions struct { @@ -41,6 +42,12 @@ func newListCommand(dockerCLI command.Cli) *cobra.Command { flags.StringVar(&options.format, "format", "", flagsHelper.FormatHelp) flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) return cmd } diff --git a/cli/command/service/logs.go b/cli/command/service/logs.go index dd56f828f9..00b8562ca4 100644 --- a/cli/command/service/logs.go +++ b/cli/command/service/logs.go @@ -11,6 +11,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/idresolver" "github.com/docker/cli/service/logs" "github.com/docker/docker/api/types" @@ -22,6 +23,7 @@ import ( "github.com/docker/docker/pkg/stringid" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) type logsOptions struct { @@ -69,6 +71,13 @@ func newLogsCommand(dockerCli command.Cli) *cobra.Command { flags.BoolVar(&opts.details, "details", false, "Show extra details provided to logs") flags.SetAnnotation("details", "version", []string{"1.30"}) flags.StringVarP(&opts.tail, "tail", "n", "all", "Number of lines to show from the end of the logs") + + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) return cmd } diff --git a/cli/command/service/ps.go b/cli/command/service/ps.go index 126d9ace2f..67d216e9a2 100644 --- a/cli/command/service/ps.go +++ b/cli/command/service/ps.go @@ -6,6 +6,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/idresolver" "github.com/docker/cli/cli/command/node" "github.com/docker/cli/cli/command/task" @@ -15,6 +16,7 @@ import ( "github.com/docker/docker/client" "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) type psOptions struct { @@ -48,6 +50,12 @@ func newPsCommand(dockerCli command.Cli) *cobra.Command { flags.StringVar(&options.format, "format", "", "Pretty-print tasks using a Go template") flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided") + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) return cmd } diff --git a/cli/command/service/rollback.go b/cli/command/service/rollback.go index 156def02bb..23752c14fa 100644 --- a/cli/command/service/rollback.go +++ b/cli/command/service/rollback.go @@ -6,9 +6,11 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/versions" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) func newRollbackCommand(dockerCli command.Cli) *cobra.Command { @@ -31,6 +33,12 @@ func newRollbackCommand(dockerCli command.Cli) *cobra.Command { flags.BoolVarP(&options.quiet, flagQuiet, "q", false, "Suppress progress output") addDetachFlag(flags, &options.detach) + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) return cmd } diff --git a/cli/command/service/update.go b/cli/command/service/update.go index a9c52f5f54..36f09bfa59 100644 --- a/cli/command/service/update.go +++ b/cli/command/service/update.go @@ -9,6 +9,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/opts" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -23,7 +24,7 @@ import ( "github.com/spf13/pflag" ) -func newUpdateCommand(dockerCli command.Cli) *cobra.Command { +func newUpdateCommand(dockerCLI command.Cli) *cobra.Command { options := newServiceOptions() cmd := &cobra.Command{ @@ -31,10 +32,10 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { Short: "Update a service", Args: cli.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runUpdate(cmd.Context(), dockerCli, cmd.Flags(), options, args[0]) + return runUpdate(cmd.Context(), dockerCLI, cmd.Flags(), options, args[0]) }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return CompletionFn(dockerCli)(cmd, args, toComplete) + return CompletionFn(dockerCLI)(cmd, args, toComplete) }, } @@ -117,6 +118,30 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command { flags.Var(newListOptsVarWithValidator(ValidateSingleGenericResource), flagGenericResourcesAdd, "Add a Generic resource") flags.SetAnnotation(flagHostAdd, "version", []string{"1.32"}) + // TODO(thaJeztah): add completion for capabilities, stop-signal (currently non-exported in container package) + // _ = cmd.RegisterFlagCompletionFunc(flagCapAdd, completeLinuxCapabilityNames) + // _ = cmd.RegisterFlagCompletionFunc(flagCapDrop, completeLinuxCapabilityNames) + // _ = cmd.RegisterFlagCompletionFunc(flagStopSignal, completeSignals) + + _ = cmd.RegisterFlagCompletionFunc(flagEnvAdd, completion.EnvVarNames) + // TODO(thaJeztah): flagEnvRemove (needs to read current env-vars on the service) + _ = cmd.RegisterFlagCompletionFunc("image", completion.ImageNames(dockerCLI, -1)) + _ = cmd.RegisterFlagCompletionFunc(flagNetworkAdd, completion.NetworkNames(dockerCLI)) + // TODO(thaJeztha): flagNetworkRemove (needs to read current list of networks from the service) + _ = cmd.RegisterFlagCompletionFunc(flagRestartCondition, completion.FromList("none", "on-failure", "any")) + _ = cmd.RegisterFlagCompletionFunc(flagRollbackOrder, completion.FromList("start-first", "stop-first")) + _ = cmd.RegisterFlagCompletionFunc(flagRollbackFailureAction, completion.FromList("pause", "continue")) + _ = cmd.RegisterFlagCompletionFunc(flagUpdateOrder, completion.FromList("start-first", "stop-first")) + _ = cmd.RegisterFlagCompletionFunc(flagUpdateFailureAction, completion.FromList("pause", "continue", "rollback")) + + completion.ImageNames(dockerCLI, -1) + flags.VisitAll(func(flag *pflag.Flag) { + // Set a default completion function if none was set. We don't look + // up if it does already have one set, because Cobra does this for + // us, and returns an error (which we ignore for this reason). + _ = cmd.RegisterFlagCompletionFunc(flag.Name, completion.NoComplete) + }) + return cmd }