diff --git a/Makefile b/Makefile index 8469c6031813..6b529faef0fb 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,16 @@ mod-outdated: ## check outdated dependencies authors: ## generate AUTHORS file from git history scripts/docs/generate-authors.sh +.PHONY: completion +completion: binary +completion: /etc/bash_completion.d/docker +completion: ## generate and install the completion scripts + +.PHONY: /etc/bash_completion.d/docker +/etc/bash_completion.d/docker: ## generate and install the bash-completion script + mkdir -p /etc/bash_completion.d + docker completion bash > /etc/bash_completion.d/docker + .PHONY: manpages manpages: ## generate man pages from go source and markdown scripts/docs/generate-man.sh diff --git a/cli/command/completion/functions.go b/cli/command/completion/functions.go index 6026ef769299..c25330a4b699 100644 --- a/cli/command/completion/functions.go +++ b/cli/command/completion/functions.go @@ -2,6 +2,7 @@ package completion import ( "os" + "strings" "github.com/docker/cli/cli/command/formatter" "github.com/docker/docker/api/types" @@ -106,6 +107,41 @@ func NetworkNames(dockerCLI APIClientProvider) ValidArgsFn { } } +// EnvVarNames offers completion for environment-variable names. This +// completion can be used for "--env" and "--build-arg" flags, which +// allow obtaining the value of the given environment-variable if present +// in the local environment, so we only should complete the names of the +// environment variables, and not their value. This also prevents the +// completion script from printing values of environment variables +// containing sensitive values. +// +// For example; +// +// export MY_VAR=hello +// docker run --rm --env MY_VAR alpine printenv MY_VAR +// hello +func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) { + envs := os.Environ() + names = make([]string, 0, len(envs)) + for _, env := range envs { + name, _, _ := strings.Cut(env, "=") + names = append(names, name) + } + return names, cobra.ShellCompDirectiveNoFileComp +} + +// FromList offers completion for the given list of options. +func FromList(options ...string) ValidArgsFn { + return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp) +} + +// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault], +// which indicates to let the shell perform its default behavior after +// completions have been provided. +func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return nil, cobra.ShellCompDirectiveDefault +} + // NoComplete is used for commands where there's no relevant completion func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveNoFileComp diff --git a/cli/command/container/completion.go b/cli/command/container/completion.go new file mode 100644 index 000000000000..24c046a8292e --- /dev/null +++ b/cli/command/container/completion.go @@ -0,0 +1,94 @@ +package container + +import ( + "github.com/docker/cli/cli/command/completion" + "github.com/docker/docker/api/types/container" + "github.com/moby/sys/signal" + "github.com/spf13/cobra" +) + +// allLinuxCapabilities is a list of all known Linux capabilities. +// +// This list was based on the containerd pkg/cap package; +// https://github.com/containerd/containerd/blob/v1.7.19/pkg/cap/cap_linux.go#L133-L181 +// +// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true") +var allLinuxCapabilities = []string{ + "ALL", // magic value for "all capabilities" + + // caps35 is the caps of kernel 3.5 (37 entries) + "CAP_CHOWN", // 2.2 + "CAP_DAC_OVERRIDE", // 2.2 + "CAP_DAC_READ_SEARCH", // 2.2 + "CAP_FOWNER", // 2.2 + "CAP_FSETID", // 2.2 + "CAP_KILL", // 2.2 + "CAP_SETGID", // 2.2 + "CAP_SETUID", // 2.2 + "CAP_SETPCAP", // 2.2 + "CAP_LINUX_IMMUTABLE", // 2.2 + "CAP_NET_BIND_SERVICE", // 2.2 + "CAP_NET_BROADCAST", // 2.2 + "CAP_NET_ADMIN", // 2.2 + "CAP_NET_RAW", // 2.2 + "CAP_IPC_LOCK", // 2.2 + "CAP_IPC_OWNER", // 2.2 + "CAP_SYS_MODULE", // 2.2 + "CAP_SYS_RAWIO", // 2.2 + "CAP_SYS_CHROOT", // 2.2 + "CAP_SYS_PTRACE", // 2.2 + "CAP_SYS_PACCT", // 2.2 + "CAP_SYS_ADMIN", // 2.2 + "CAP_SYS_BOOT", // 2.2 + "CAP_SYS_NICE", // 2.2 + "CAP_SYS_RESOURCE", // 2.2 + "CAP_SYS_TIME", // 2.2 + "CAP_SYS_TTY_CONFIG", // 2.2 + "CAP_MKNOD", // 2.4 + "CAP_LEASE", // 2.4 + "CAP_AUDIT_WRITE", // 2.6.11 + "CAP_AUDIT_CONTROL", // 2.6.11 + "CAP_SETFCAP", // 2.6.24 + "CAP_MAC_OVERRIDE", // 2.6.25 + "CAP_MAC_ADMIN", // 2.6.25 + "CAP_SYSLOG", // 2.6.37 + "CAP_WAKE_ALARM", // 3.0 + "CAP_BLOCK_SUSPEND", // 3.5 + + // caps316 is the caps of kernel 3.16 (38 entries) + "CAP_AUDIT_READ", + + // caps58 is the caps of kernel 5.8 (40 entries) + "CAP_PERFMON", + "CAP_BPF", + + // caps59 is the caps of kernel 5.9 (41 entries) + "CAP_CHECKPOINT_RESTORE", +} + +// restartPolicies is a list of all valid restart-policies.. +// +// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true") +var restartPolicies = []string{ + string(container.RestartPolicyDisabled), + string(container.RestartPolicyAlways), + string(container.RestartPolicyOnFailure), + string(container.RestartPolicyUnlessStopped), +} + +func completeLinuxCapabilityNames(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) { + return completion.FromList(allLinuxCapabilities...)(cmd, args, toComplete) +} + +func completeRestartPolicies(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) { + return completion.FromList(restartPolicies...)(cmd, args, toComplete) +} + +func completeSignals(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) { + // TODO(thaJeztah): do we want to provide the full list here, or a subset? + signalNames := make([]string, 0, len(signal.SignalMap)) + for k := range signal.SignalMap { + signalNames = append(signalNames, k) + } + return completion.FromList(signalNames...)(cmd, args, toComplete) +} diff --git a/cli/command/container/create.go b/cli/command/container/create.go index 3193ccf20072..0e449bb68a6e 100644 --- a/cli/command/container/create.go +++ b/cli/command/container/create.go @@ -77,6 +77,16 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command { command.AddPlatformFlag(flags, &options.platform) command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) copts = addFlags(flags) + + _ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames) + _ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames) + _ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames) + _ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames) + _ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli)) + _ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever)) + _ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies) + _ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals) + _ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true)) return cmd } diff --git a/cli/command/container/exec.go b/cli/command/container/exec.go index 1c0a741263cf..8244734a5cb7 100644 --- a/cli/command/container/exec.go +++ b/cli/command/container/exec.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "os" "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" @@ -79,12 +78,8 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command { flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container") flags.SetAnnotation("workdir", "version", []string{"1.35"}) - _ = cmd.RegisterFlagCompletionFunc("env", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { - return os.Environ(), cobra.ShellCompDirectiveNoFileComp - }) - _ = cmd.RegisterFlagCompletionFunc("env-file", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveDefault // _filedir - }) + _ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames) + _ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames) return cmd } diff --git a/cli/command/container/kill.go b/cli/command/container/kill.go index 96ce22e5ae85..0095198b5aab 100644 --- a/cli/command/container/kill.go +++ b/cli/command/container/kill.go @@ -38,6 +38,9 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container") + + _ = cmd.RegisterFlagCompletionFunc("signal", completeSignals) + return cmd } @@ -50,7 +53,7 @@ func runKill(ctx context.Context, dockerCli command.Cli, opts *killOptions) erro if err := <-errChan; err != nil { errs = append(errs, err.Error()) } else { - fmt.Fprintln(dockerCli.Out(), name) + _, _ = fmt.Fprintln(dockerCli.Out(), name) } } if len(errs) > 0 { diff --git a/cli/command/container/restart.go b/cli/command/container/restart.go index 0ac4fa985ef7..8e7c0d4a026f 100644 --- a/cli/command/container/restart.go +++ b/cli/command/container/restart.go @@ -43,6 +43,9 @@ func NewRestartCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container") flags.IntVarP(&opts.timeout, "time", "t", 0, "Seconds to wait before killing the container") + + _ = cmd.RegisterFlagCompletionFunc("signal", completeSignals) + return cmd } diff --git a/cli/command/container/run.go b/cli/command/container/run.go index 562e8029208b..ef0986cba281 100644 --- a/cli/command/container/run.go +++ b/cli/command/container/run.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "io" - "os" "strings" "syscall" @@ -70,22 +69,15 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command { command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled()) copts = addFlags(flags) - cmd.RegisterFlagCompletionFunc( - "env", - func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return os.Environ(), cobra.ShellCompDirectiveNoFileComp - }, - ) - cmd.RegisterFlagCompletionFunc( - "env-file", - func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveDefault - }, - ) - cmd.RegisterFlagCompletionFunc( - "network", - completion.NetworkNames(dockerCli), - ) + _ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames) + _ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames) + _ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames) + _ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames) + _ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli)) + _ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever)) + _ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies) + _ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals) + _ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true)) return cmd } diff --git a/cli/command/container/stop.go b/cli/command/container/stop.go index c221fb4448ac..c99a7c456dbc 100644 --- a/cli/command/container/stop.go +++ b/cli/command/container/stop.go @@ -43,6 +43,9 @@ func NewStopCommand(dockerCli command.Cli) *cobra.Command { flags := cmd.Flags() flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container") flags.IntVarP(&opts.timeout, "time", "t", 0, "Seconds to wait before killing the container") + + _ = cmd.RegisterFlagCompletionFunc("signal", completeSignals) + return cmd } diff --git a/cli/command/container/update.go b/cli/command/container/update.go index 7ea668830b1b..5720f39bceaa 100644 --- a/cli/command/container/update.go +++ b/cli/command/container/update.go @@ -83,6 +83,8 @@ func NewUpdateCommand(dockerCli command.Cli) *cobra.Command { flags.Var(&options.cpus, "cpus", "Number of CPUs") flags.SetAnnotation("cpus", "version", []string{"1.29"}) + _ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies) + return cmd } diff --git a/cli/command/image/history.go b/cli/command/image/history.go index e36b9bc03cb8..1c0ae409811a 100644 --- a/cli/command/image/history.go +++ b/cli/command/image/history.go @@ -5,6 +5,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/spf13/cobra" @@ -31,6 +32,7 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command { opts.image = args[0] return runHistory(cmd.Context(), dockerCli, opts) }, + ValidArgsFunction: completion.ImageNames(dockerCli), Annotations: map[string]string{ "aliases": "docker image history, docker history", }, diff --git a/cli/command/image/inspect.go b/cli/command/image/inspect.go index 96b3bb8f4cbd..90fc6704008d 100644 --- a/cli/command/image/inspect.go +++ b/cli/command/image/inspect.go @@ -8,6 +8,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/inspect" flagsHelper "github.com/docker/cli/cli/flags" "github.com/spf13/cobra" @@ -30,6 +31,7 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command { opts.refs = args return runInspect(cmd.Context(), dockerCli, opts) }, + ValidArgsFunction: completion.ImageNames(dockerCli), } flags := cmd.Flags() diff --git a/cli/command/image/remove.go b/cli/command/image/remove.go index e0983f377f9b..3fb0af5c03de 100644 --- a/cli/command/image/remove.go +++ b/cli/command/image/remove.go @@ -7,6 +7,7 @@ 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/image" "github.com/docker/docker/errdefs" "github.com/pkg/errors" @@ -29,6 +30,7 @@ func NewRemoveCommand(dockerCli command.Cli) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { return runRemove(cmd.Context(), dockerCli, opts, args) }, + ValidArgsFunction: completion.ImageNames(dockerCli), Annotations: map[string]string{ "aliases": "docker image rm, docker image remove, docker rmi", }, diff --git a/cli/context/store/store.go b/cli/context/store/store.go index 44e9477fb014..066b5769d728 100644 --- a/cli/context/store/store.go +++ b/cli/context/store/store.go @@ -124,6 +124,9 @@ func (s *ContextStore) List() ([]Metadata, error) { // Names return Metadata names for a Lister func Names(s Lister) ([]string, error) { + if s == nil { + return nil, errors.New("nil lister") + } list, err := s.List() if err != nil { return nil, err diff --git a/cli/context/store/store_test.go b/cli/context/store/store_test.go index ad61036dbe52..c785b2e01524 100644 --- a/cli/context/store/store_test.go +++ b/cli/context/store/store_test.go @@ -260,3 +260,9 @@ func TestCorruptMetadata(t *testing.T) { _, err = s.GetMetadata("source") assert.ErrorContains(t, err, fmt.Sprintf("parsing %s: unexpected end of JSON input", contextFile)) } + +func TestNames(t *testing.T) { + names, err := Names(nil) + assert.Check(t, is.Error(err, "nil lister")) + assert.Check(t, is.Len(names, 0)) +} diff --git a/cmd/docker/completions.go b/cmd/docker/completions.go index 582a2051e33d..95ee737b4276 100644 --- a/cmd/docker/completions.go +++ b/cmd/docker/completions.go @@ -1,31 +1,24 @@ package main import ( + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/context/store" "github.com/spf13/cobra" ) -func registerCompletionFuncForGlobalFlags(contextStore store.Store, cmd *cobra.Command) error { - err := cmd.RegisterFlagCompletionFunc( - "context", - func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - names, err := store.Names(contextStore) - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - return names, cobra.ShellCompDirectiveNoFileComp - }, - ) +type contextStoreProvider interface { + ContextStore() store.Store +} + +func registerCompletionFuncForGlobalFlags(dockerCLI contextStoreProvider, cmd *cobra.Command) error { + err := cmd.RegisterFlagCompletionFunc("context", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { + names, _ := store.Names(dockerCLI.ContextStore()) + return names, cobra.ShellCompDirectiveNoFileComp + }) if err != nil { return err } - err = cmd.RegisterFlagCompletionFunc( - "log-level", - func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - values := []string{"debug", "info", "warn", "error", "fatal"} - return values, cobra.ShellCompDirectiveNoFileComp - }, - ) + err = cmd.RegisterFlagCompletionFunc("log-level", completion.FromList("debug", "info", "warn", "error", "fatal")) if err != nil { return err } diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index 7825c7d35455..6e28a99ebc51 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -102,7 +102,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand { cmd.SetErr(dockerCli.Err()) opts, helpCmd = cli.SetupRootCommand(cmd) - _ = registerCompletionFuncForGlobalFlags(dockerCli.ContextStore(), cmd) + _ = registerCompletionFuncForGlobalFlags(dockerCli, cmd) cmd.Flags().BoolP("version", "v", false, "Print version information and quit") setFlagErrorFunc(dockerCli, cmd) diff --git a/dockerfiles/Dockerfile.dev b/dockerfiles/Dockerfile.dev index 365c5e4216b4..f387763e4625 100644 --- a/dockerfiles/Dockerfile.dev +++ b/dockerfiles/Dockerfile.dev @@ -35,6 +35,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \ FROM golang AS dev RUN apk add --no-cache \ bash \ + bash-completion \ build-base \ ca-certificates \ coreutils \ @@ -44,7 +45,8 @@ RUN apk add --no-cache \ nano RUN echo -e "\nYou are now in a development container. Run '\e\033[1mmake help\e\033[0m' to learn about\navailable make targets.\n" > /etc/motd \ - && echo -e "cat /etc/motd\nPS1=\"\e[0;32m\u@docker-cli-dev\\$ \e[0m\"" >> /root/.bashrc + && echo -e "cat /etc/motd\nPS1=\"\e[0;32m\u@docker-cli-dev\\$ \e[0m\"" >> /root/.bashrc \ + && echo -e "source /etc/bash/bash_completion.sh" >> /root/.bashrc CMD bash ENV DISABLE_WARN_OUTSIDE_CONTAINER=1 ENV PATH=$PATH:/go/src/github.com/docker/cli/build