Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

various improvements to shell completions #5238

Merged
merged 11 commits into from
Jul 17, 2024
36 changes: 36 additions & 0 deletions cli/command/completion/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package completion

import (
"os"
"strings"

"github.com/docker/cli/cli/command/formatter"
"github.com/docker/docker/api/types/container"
Expand Down Expand Up @@ -105,6 +106,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
Expand Down
94 changes: 94 additions & 0 deletions cli/command/container/completion.go
Original file line number Diff line number Diff line change
@@ -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")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check why we disable it here;

cli/cmd/docker/docker.go

Lines 92 to 96 in 9bb1a62

CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: false,
HiddenDefaultCmd: true,
DisableDescriptions: true,
},

And here;

CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: false,
HiddenDefaultCmd: true,
DisableDescriptions: true,
},

var allLinuxCapabilities = []string{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like containerd/pkg/cap maintains a list of these: https://github.com/containerd/containerd/blob/78d3e205a51ec101f775a43bee6f4fdd8fc6b22b/pkg/cap/cap_linux.go#L133-L187

Perhaps we could help extracting this from pkg to a separate repo that we could consume?

(not a blocker for this PR though)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like containerd/pkg/cap maintains a list of these:

Hehe, yes, look at the comment above; that's where I took the list from 😄.

We could consider moving such things to a separate repo (or to be in a smaller module); one tricky bit there is that these are linux capabilities, so if we want to use them cross-platform (in our case, the CLI may be running on Windows), at least we'd have to make clear that they only apply to Linux, so we can't have that code be platform-specific at compile time (_linux.go e.g.).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, I didn't read the comment 🤦🏻 😄

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we can't use it in the current form, but would be nice to avoid having to maintain this list by ourselves in future.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah agreed. For this one I decided that it's not critical for the list to be up-to-date (even more accurate; completion may provide capabilities that are not supported by the daemon host if it's running an older kernel version), but for completion that should be mostly "OK"; the user doesn't have to use the completion, and can still type something else.

For the --cap-add and --cap-rm cases, there's also the case that it would provide suggestions for capabilities that are either already set by default, or not present (both of which should be a no-op).

So the most accurate thing to do would be to have API endpoints for these (i.e., let the daemon provide options that can be used) that are specifically crafted for purpose of completion (as lightweight as possible), but even if they're lightweight, there may still be some latency when connected to a remove machine, so that's something we should explore and consider pros/cons.

"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)
}
10 changes: 10 additions & 0 deletions cli/command/container/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
9 changes: 2 additions & 7 deletions cli/command/container/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
"os"

"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
Expand Down Expand Up @@ -78,12 +77,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
}
Expand Down
5 changes: 4 additions & 1 deletion cli/command/container/kill.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions cli/command/container/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
26 changes: 9 additions & 17 deletions cli/command/container/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
"os"
"strings"
"syscall"

Expand Down Expand Up @@ -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
}

Expand Down
3 changes: 3 additions & 0 deletions cli/command/container/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 2 additions & 0 deletions cli/command/container/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
3 changes: 3 additions & 0 deletions cli/context/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions cli/context/store/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
29 changes: 11 additions & 18 deletions cmd/docker/completions.go
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,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)

Expand Down
Loading