diff --git a/cli/command/system/client_test.go b/cli/command/system/client_test.go index b6eeb3bd92..3c9aa290e7 100644 --- a/cli/command/system/client_test.go +++ b/cli/command/system/client_test.go @@ -19,6 +19,7 @@ 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) } func (cli *fakeClient) ServerVersion(ctx context.Context) (types.Version, error) { @@ -46,3 +47,10 @@ 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 +} diff --git a/cli/command/system/completion.go b/cli/command/system/completion.go new file mode 100644 index 0000000000..eaad9b343f --- /dev/null +++ b/cli/command/system/completion.go @@ -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 +} diff --git a/cli/command/system/completion_test.go b/cli/command/system/completion_test.go new file mode 100644 index 0000000000..59aa546a08 --- /dev/null +++ b/cli/command/system/completion_test.go @@ -0,0 +1,45 @@ +package system + +import ( + "context" + "errors" + "testing" + + "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" +) + +// Successful completion lists all container names, prefixed with "container=". +// Filtering the completions by the current word is delegated to the completion script. +func TestCompleteEventFilterContainer(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + containerListFunc: func(_ context.Context, _ container.ListOptions) ([]container.Summary, error) { + return []container.Summary{ + *builders.Container("foo"), + *builders.Container("bar"), + }, nil + }, + }) + + completions, directive := completeFilters(cli)(NewEventsCommand(cli), nil, "container=") + + assert.DeepEqual(t, completions, []string{"container=foo", "container=bar"}) + assert.Equal(t, directive, cobra.ShellCompDirectiveNoFileComp) +} + +// In case of API errors, no completions are returned. +func TestCompleteEventFilterContainerAPIError(t *testing.T) { + cli := test.NewFakeCli(&fakeClient{ + containerListFunc: func(_ context.Context, _ container.ListOptions) ([]container.Summary, error) { + return nil, errors.New("API error") + }, + }) + + completions, directive := completeFilters(cli)(NewEventsCommand(cli), nil, "container=") + + assert.DeepEqual(t, completions, []string{}) + assert.Equal(t, directive, cobra.ShellCompDirectiveNoFileComp) +} diff --git a/cli/command/system/events.go b/cli/command/system/events.go index a54d30ffed..e31256e215 100644 --- a/cli/command/system/events.go +++ b/cli/command/system/events.go @@ -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 }