package container import ( "strings" "sync" "github.com/docker/cli/cli/command/completion" "github.com/docker/docker/api/types/container" "github.com/moby/sys/capability" "github.com/moby/sys/signal" "github.com/spf13/cobra" ) // allCaps is the magic value for "all capabilities". const allCaps = "ALL" // allLinuxCapabilities is a list of all known Linux capabilities. // // TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true") // TODO(thaJeztah): consider what casing we want to use for completion (see below); // // We need to consider what format is most convenient; currently we use the // canonical name (uppercase and "CAP_" prefix), however, tab-completion is // case-sensitive by default, so requires the user to type uppercase letters // to filter the list of options. // // Bash completion provides a `completion-ignore-case on` option to make completion // case-insensitive (https://askubuntu.com/a/87066), but it looks to be a global // option; the current cobra.CompletionOptions also don't provide this as an option // to be used in the generated completion-script. // // Fish completion has `smartcase` (by default?) which matches any case if // all of the input is lowercase. // // Zsh does not appear have a dedicated option, but allows setting matching-rules // (see https://superuser.com/a/1092328). var allLinuxCapabilities = sync.OnceValue(func() []string { caps := capability.ListKnown() out := make([]string, 0, len(caps)+1) out = append(out, allCaps) for _, c := range caps { out = append(out, "CAP_"+strings.ToUpper(c.String())) } return out }) // logDriverOptions provides the options for each built-in logging driver. var logDriverOptions = map[string][]string{ "awslogs": { "max-buffer-size", "mode", "awslogs-create-group", "awslogs-credentials-endpoint", "awslogs-datetime-format", "awslogs-group", "awslogs-multiline-pattern", "awslogs-region", "awslogs-stream", "tag", }, "fluentd": { "max-buffer-size", "mode", "env", "env-regex", "labels", "fluentd-address", "fluentd-async", "fluentd-buffer-limit", "fluentd-request-ack", "fluentd-retry-wait", "fluentd-max-retries", "fluentd-sub-second-precision", "tag", }, "gcplogs": { "max-buffer-size", "mode", "env", "env-regex", "labels", "gcp-log-cmd", "gcp-meta-id", "gcp-meta-name", "gcp-meta-zone", "gcp-project", }, "gelf": { "max-buffer-size", "mode", "env", "env-regex", "labels", "gelf-address", "gelf-compression-level", "gelf-compression-type", "gelf-tcp-max-reconnect", "gelf-tcp-reconnect-delay", "tag", }, "journald": {"max-buffer-size", "mode", "env", "env-regex", "labels", "tag"}, "json-file": {"max-buffer-size", "mode", "env", "env-regex", "labels", "compress", "max-file", "max-size"}, "local": {"max-buffer-size", "mode", "compress", "max-file", "max-size"}, "none": {}, "splunk": { "max-buffer-size", "mode", "env", "env-regex", "labels", "splunk-caname", "splunk-capath", "splunk-format", "splunk-gzip", "splunk-gzip-level", "splunk-index", "splunk-insecureskipverify", "splunk-source", "splunk-sourcetype", "splunk-token", "splunk-url", "splunk-verify-connection", "tag", }, "syslog": { "max-buffer-size", "mode", "env", "env-regex", "labels", "syslog-address", "syslog-facility", "syslog-format", "syslog-tls-ca-cert", "syslog-tls-cert", "syslog-tls-key", "syslog-tls-skip-verify", "tag", }, } // builtInLogDrivers provides a list of the built-in logging drivers. var builtInLogDrivers = sync.OnceValue(func() []string { drivers := make([]string, 0, len(logDriverOptions)) for driver := range logDriverOptions { drivers = append(drivers, driver) } return drivers }) // allLogDriverOptions provides all options of the built-in logging drivers. // The list does not contain duplicates. var allLogDriverOptions = sync.OnceValue(func() []string { var result []string seen := make(map[string]bool) for driver := range logDriverOptions { for _, opt := range logDriverOptions[driver] { if !seen[opt] { seen[opt] = true result = append(result, opt) } } } return result }) // 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), } // addCompletions adds the completions that `run` and `create` have in common. func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider) { _ = cmd.RegisterFlagCompletionFunc("attach", completion.FromList("stderr", "stdin", "stdout")) _ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames) _ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames) _ = cmd.RegisterFlagCompletionFunc("cgroupns", completeCgroupns()) _ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames) _ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames) _ = cmd.RegisterFlagCompletionFunc("ipc", completeIpc(dockerCLI)) _ = cmd.RegisterFlagCompletionFunc("link", completeLink(dockerCLI)) _ = cmd.RegisterFlagCompletionFunc("log-driver", completeLogDriver(dockerCLI)) _ = cmd.RegisterFlagCompletionFunc("log-opt", completeLogOpt) _ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCLI)) _ = cmd.RegisterFlagCompletionFunc("pid", completePid(dockerCLI)) _ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms) _ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever)) _ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies) _ = cmd.RegisterFlagCompletionFunc("security-opt", completeSecurityOpt) _ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals) _ = cmd.RegisterFlagCompletionFunc("storage-opt", completeStorageOpt) _ = cmd.RegisterFlagCompletionFunc("ulimit", completeUlimit) _ = cmd.RegisterFlagCompletionFunc("userns", completion.FromList("host")) _ = cmd.RegisterFlagCompletionFunc("uts", completion.FromList("host")) _ = cmd.RegisterFlagCompletionFunc("volume-driver", completeVolumeDriver(dockerCLI)) _ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCLI, true)) } // completeCgroupns implements shell completion for the `--cgroupns` option of `run` and `create`. func completeCgroupns() completion.ValidArgsFn { return completion.FromList(string(container.CgroupnsModeHost), string(container.CgroupnsModePrivate)) } // completeDetachKeys implements shell completion for the `--detach-keys` option of `run` and `create`. func completeDetachKeys(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return []string{"ctrl-"}, cobra.ShellCompDirectiveNoSpace } // completeIpc implements shell completion for the `--ipc` option of `run` and `create`. // The completion is partly composite. func completeIpc(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container" return []string{"container:"}, cobra.ShellCompDirectiveNoSpace } if strings.HasPrefix(toComplete, "container:") { names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete) return prefixWith("container:", names), cobra.ShellCompDirectiveNoFileComp } return []string{ string(container.IPCModeContainer + ":"), string(container.IPCModeHost), string(container.IPCModeNone), string(container.IPCModePrivate), string(container.IPCModeShareable), }, cobra.ShellCompDirectiveNoFileComp } } // completeLink implements shell completion for the `--link` option of `run` and `create`. func completeLink(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return postfixWith(":", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoSpace } } // completeLogDriver implements shell completion for the `--log-driver` option of `run` and `create`. // The log drivers are collected from a call to the Info endpoint with a fallback to a hard-coded list // of the build-in log drivers. func completeLogDriver(dockerCLI completion.APIClientProvider) completion.ValidArgsFn { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { info, err := dockerCLI.Client().Info(cmd.Context()) if err != nil { return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp } drivers := info.Plugins.Log return drivers, cobra.ShellCompDirectiveNoFileComp } } // completeLogOpt implements shell completion for the `--log-opt` option of `run` and `create`. // If the user supplied a log-driver, only options for that driver are returned. func completeLogOpt(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { driver, _ := cmd.Flags().GetString("log-driver") if options, exists := logDriverOptions[driver]; exists { return postfixWith("=", options), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp } return postfixWith("=", allLogDriverOptions()), cobra.ShellCompDirectiveNoSpace } // completePid implements shell completion for the `--pid` option of `run` and `create`. func completePid(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container" return []string{"container:"}, cobra.ShellCompDirectiveNoSpace } if strings.HasPrefix(toComplete, "container:") { names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete) return prefixWith("container:", names), cobra.ShellCompDirectiveNoFileComp } return []string{"container:", "host"}, cobra.ShellCompDirectiveNoFileComp } } // completeSecurityOpt implements shell completion for the `--security-opt` option of `run` and `create`. // The completion is partly composite. func completeSecurityOpt(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(toComplete) > 0 && strings.HasPrefix("apparmor=", toComplete) { //nolint:gocritic // not swapped, matches partly typed "apparmor=" return []string{"apparmor="}, cobra.ShellCompDirectiveNoSpace } if len(toComplete) > 0 && strings.HasPrefix("label", toComplete) { //nolint:gocritic // not swapped, matches partly typed "label" return []string{"label="}, cobra.ShellCompDirectiveNoSpace } if strings.HasPrefix(toComplete, "label=") { if strings.HasPrefix(toComplete, "label=d") { return []string{"label=disable"}, cobra.ShellCompDirectiveNoFileComp } labels := []string{"disable", "level:", "role:", "type:", "user:"} return prefixWith("label=", labels), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp } // length must be > 1 here so that completion of "s" falls through. if len(toComplete) > 1 && strings.HasPrefix("seccomp", toComplete) { //nolint:gocritic // not swapped, matches partly typed "seccomp" return []string{"seccomp="}, cobra.ShellCompDirectiveNoSpace } if strings.HasPrefix(toComplete, "seccomp=") { return []string{"seccomp=unconfined"}, cobra.ShellCompDirectiveNoFileComp } return []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"}, cobra.ShellCompDirectiveNoFileComp } // completeStorageOpt implements shell completion for the `--storage-opt` option of `run` and `create`. func completeStorageOpt(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { return []string{"size="}, cobra.ShellCompDirectiveNoSpace } // completeUlimit implements shell completion for the `--ulimit` option of `run` and `create`. func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { limits := []string{ "as", "chroot", "core", "cpu", "data", "fsize", "locks", "maxlogins", "maxsyslogins", "memlock", "msgqueue", "nice", "nofile", "nproc", "priority", "rss", "rtprio", "sigpending", "stack", } return postfixWith("=", limits), cobra.ShellCompDirectiveNoSpace } // completeVolumeDriver contacts the API to get the built-in and installed volume drivers. func completeVolumeDriver(dockerCLI completion.APIClientProvider) completion.ValidArgsFn { return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { info, err := dockerCLI.Client().Info(cmd.Context()) if err != nil { // fallback: the built-in drivers return []string{"local"}, cobra.ShellCompDirectiveNoFileComp } drivers := info.Plugins.Volume return drivers, cobra.ShellCompDirectiveNoFileComp } } // 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 } // prefixWith prefixes every element in the slice with the given prefix. func prefixWith(prefix string, values []string) []string { result := make([]string, len(values)) for i, v := range values { result[i] = prefix + v } return result } // postfixWith appends postfix to every element in the slice. func postfixWith(postfix string, values []string) []string { result := make([]string, len(values)) for i, v := range values { result[i] = v + postfix } return result } 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) }