mirror of
https://github.com/docker/cli.git
synced 2026-08-24 02:24:17 -05:00
Merge pull request #7124 from JessThrysoee/completion-swarm-filters
completion: add completion for swarm "--filter" flags
This commit is contained in:
@@ -172,6 +172,28 @@ func FromList(options ...string) cobra.CompletionFunc {
|
||||
return Unique(cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp))
|
||||
}
|
||||
|
||||
// WithPrefix prefixes every element in the slice with the given prefix.
|
||||
// It is a helper for building "--filter" completions, where each candidate
|
||||
// value is offered as "key=value".
|
||||
func WithPrefix(prefix string, values []string) []string {
|
||||
result := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
result[i] = prefix + v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// WithSuffix appends the given suffix to every element in the slice. It is a
|
||||
// helper for building "--filter" completions, where filter keys are offered
|
||||
// with a trailing "=" (combined with [cobra.ShellCompDirectiveNoSpace]).
|
||||
func WithSuffix(suffix string, values []string) []string {
|
||||
result := make([]string, len(values))
|
||||
for i, v := range values {
|
||||
result[i] = v + suffix
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
|
||||
// which indicates to let the shell perform its default behavior after
|
||||
// completions have been provided.
|
||||
|
||||
@@ -196,6 +196,18 @@ func TestCompleteFromList(t *testing.T) {
|
||||
assert.Check(t, is.DeepEqual(values, expected))
|
||||
}
|
||||
|
||||
func TestWithPrefix(t *testing.T) {
|
||||
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{"n1", "n2"}), []string{"node=n1", "node=n2"}))
|
||||
assert.Check(t, is.DeepEqual(WithPrefix("node=", []string{}), []string{}))
|
||||
assert.Check(t, is.DeepEqual(WithPrefix("", []string{"n1"}), []string{"n1"}))
|
||||
}
|
||||
|
||||
func TestWithSuffix(t *testing.T) {
|
||||
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{"id", "name"}), []string{"id=", "name="}))
|
||||
assert.Check(t, is.DeepEqual(WithSuffix("=", []string{}), []string{}))
|
||||
assert.Check(t, is.DeepEqual(WithSuffix("", []string{"id"}), []string{"id"}))
|
||||
}
|
||||
|
||||
func TestCompleteImageNames(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
|
||||
@@ -2,12 +2,26 @@ package node
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// nodePsFilters are the filters that can be used with "docker node ps --filter".
|
||||
nodePsFilters = []string{"desired-state", "id", "label", "name"}
|
||||
|
||||
// taskDesiredStates are the valid values for the "desired-state" task filter.
|
||||
taskDesiredStates = []string{
|
||||
string(swarm.TaskStateRunning),
|
||||
string(swarm.TaskStateShutdown),
|
||||
string(swarm.TaskStateAccepted),
|
||||
}
|
||||
)
|
||||
|
||||
// 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.
|
||||
@@ -35,3 +49,23 @@ func completeNodeNames(dockerCLI completion.APIClientProvider) cobra.CompletionF
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// completeNodePsFilters provides completion for the filters that can be used
|
||||
// with "docker node ps --filter".
|
||||
func completeNodePsFilters(_ completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
key, _, ok := strings.Cut(toComplete, "=")
|
||||
if !ok {
|
||||
return completion.WithSuffix("=", nodePsFilters), cobra.ShellCompDirectiveNoSpace
|
||||
}
|
||||
switch key {
|
||||
case "desired-state":
|
||||
return completion.WithPrefix("desired-state=", taskDesiredStates), cobra.ShellCompDirectiveNoFileComp
|
||||
case "id", "name", "label":
|
||||
// Task IDs, names, and labels are not easily discoverable; only offer the key.
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
default:
|
||||
return completion.WithSuffix("=", nodePsFilters), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package node
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestCompleteNodePsFilters(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
toComplete string
|
||||
expected []string
|
||||
directive cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
doc: "no input offers the filter keys",
|
||||
toComplete: "",
|
||||
expected: []string{"desired-state=", "id=", "label=", "name="},
|
||||
directive: cobra.ShellCompDirectiveNoSpace,
|
||||
},
|
||||
{
|
||||
doc: "desired-state values",
|
||||
toComplete: "desired-state=",
|
||||
expected: []string{"desired-state=running", "desired-state=shutdown", "desired-state=accepted"},
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "label offers no values",
|
||||
toComplete: "label=",
|
||||
expected: nil,
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "unknown key falls back to the filter keys",
|
||||
toComplete: "bogus=",
|
||||
expected: []string{"desired-state=", "id=", "label=", "name="},
|
||||
directive: cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(&fakeClient{})
|
||||
completions, directive := completeNodePsFilters(cli)(newPsCommand(cli), nil, tc.toComplete)
|
||||
assert.DeepEqual(t, completions, tc.expected)
|
||||
assert.Equal(t, directive, tc.directive)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@ 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")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("filter", completeNodePsFilters(dockerCLI))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,32 @@ package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
// serviceListFilters are the filters that can be used with "docker service ls --filter".
|
||||
serviceListFilters = []string{"id", "label", "mode", "name"}
|
||||
|
||||
// serviceModes are the valid values for the "mode" filter of "docker service ls".
|
||||
serviceModes = []string{"replicated", "global", "replicated-job", "global-job"}
|
||||
|
||||
// servicePsFilters are the filters that can be used with "docker service ps --filter".
|
||||
servicePsFilters = []string{"desired-state", "id", "name", "node"}
|
||||
|
||||
// taskDesiredStates are the valid values for the "desired-state" task filter.
|
||||
taskDesiredStates = []string{
|
||||
string(swarm.TaskStateRunning),
|
||||
string(swarm.TaskStateShutdown),
|
||||
string(swarm.TaskStateAccepted),
|
||||
}
|
||||
)
|
||||
|
||||
// completeServiceNames 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.
|
||||
@@ -31,3 +51,74 @@ func completeServiceNames(dockerCLI completion.APIClientProvider) cobra.Completi
|
||||
return names, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
|
||||
// completeServiceListFilters provides completion for the filters that can be
|
||||
// used with "docker service ls --filter".
|
||||
func completeServiceListFilters(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
key, _, ok := strings.Cut(toComplete, "=")
|
||||
if !ok {
|
||||
return completion.WithSuffix("=", serviceListFilters), cobra.ShellCompDirectiveNoSpace
|
||||
}
|
||||
switch key {
|
||||
case "id", "name":
|
||||
return completion.WithPrefix(key+"=", serviceNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
|
||||
case "mode":
|
||||
return completion.WithPrefix("mode=", serviceModes), cobra.ShellCompDirectiveNoFileComp
|
||||
case "label":
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
default:
|
||||
return completion.WithSuffix("=", serviceListFilters), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// completeServicePsFilters provides completion for the filters that can be
|
||||
// used with "docker service ps --filter".
|
||||
func completeServicePsFilters(dockerCLI completion.APIClientProvider) cobra.CompletionFunc {
|
||||
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
key, _, ok := strings.Cut(toComplete, "=")
|
||||
if !ok {
|
||||
return completion.WithSuffix("=", servicePsFilters), cobra.ShellCompDirectiveNoSpace
|
||||
}
|
||||
switch key {
|
||||
case "desired-state":
|
||||
return completion.WithPrefix("desired-state=", taskDesiredStates), cobra.ShellCompDirectiveNoFileComp
|
||||
case "node":
|
||||
return completion.WithPrefix("node=", nodeNames(dockerCLI, cmd)), cobra.ShellCompDirectiveNoFileComp
|
||||
case "id", "name":
|
||||
// Task IDs and names are not easily discoverable; only offer the key.
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
default:
|
||||
return completion.WithSuffix("=", servicePsFilters), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serviceNames contacts the API to get a list of service names.
|
||||
// In case of an error, an empty list is returned.
|
||||
func serviceNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
|
||||
res, err := dockerCLI.Client().ServiceList(cmd.Context(), client.ServiceListOptions{})
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
names := make([]string, 0, len(res.Items))
|
||||
for _, service := range res.Items {
|
||||
names = append(names, service.Spec.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
// nodeNames contacts the API to get a list of node (host)names.
|
||||
// In case of an error, an empty list is returned.
|
||||
func nodeNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command) []string {
|
||||
res, err := dockerCLI.Client().NodeList(cmd.Context(), client.NodeListOptions{})
|
||||
if err != nil {
|
||||
return []string{}
|
||||
}
|
||||
names := make([]string, 0, len(res.Items))
|
||||
for _, node := range res.Items {
|
||||
names = append(names, node.Description.Hostname)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/cli/internal/test/builders"
|
||||
"github.com/moby/moby/api/types/swarm"
|
||||
"github.com/moby/moby/client"
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func TestCompleteServicePsFilters(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
client *fakeClient
|
||||
toComplete string
|
||||
expected []string
|
||||
directive cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
doc: "no input offers the filter keys",
|
||||
toComplete: "",
|
||||
expected: []string{"desired-state=", "id=", "name=", "node="},
|
||||
directive: cobra.ShellCompDirectiveNoSpace,
|
||||
},
|
||||
{
|
||||
doc: "desired-state values",
|
||||
toComplete: "desired-state=",
|
||||
expected: []string{"desired-state=running", "desired-state=shutdown", "desired-state=accepted"},
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "node values",
|
||||
client: &fakeClient{
|
||||
nodeListFunc: func(_ context.Context, _ client.NodeListOptions) (client.NodeListResult, error) {
|
||||
return client.NodeListResult{
|
||||
Items: []swarm.Node{
|
||||
*builders.Node(builders.Hostname("n1")),
|
||||
*builders.Node(builders.Hostname("n2")),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
toComplete: "node=",
|
||||
expected: []string{"node=n1", "node=n2"},
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "node values on API error",
|
||||
client: &fakeClient{
|
||||
nodeListFunc: func(_ context.Context, _ client.NodeListOptions) (client.NodeListResult, error) {
|
||||
return client.NodeListResult{}, errors.New("API error")
|
||||
},
|
||||
},
|
||||
toComplete: "node=",
|
||||
expected: []string{},
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "id offers no values",
|
||||
toComplete: "id=",
|
||||
expected: nil,
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "unknown key falls back to the filter keys",
|
||||
toComplete: "bogus=",
|
||||
expected: []string{"desired-state=", "id=", "name=", "node="},
|
||||
directive: cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(tc.client)
|
||||
completions, directive := completeServicePsFilters(cli)(newPsCommand(cli), nil, tc.toComplete)
|
||||
assert.DeepEqual(t, completions, tc.expected)
|
||||
assert.Equal(t, directive, tc.directive)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompleteServiceListFilters(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
client *fakeClient
|
||||
toComplete string
|
||||
expected []string
|
||||
directive cobra.ShellCompDirective
|
||||
}{
|
||||
{
|
||||
doc: "no input offers the filter keys",
|
||||
toComplete: "",
|
||||
expected: []string{"id=", "label=", "mode=", "name="},
|
||||
directive: cobra.ShellCompDirectiveNoSpace,
|
||||
},
|
||||
{
|
||||
doc: "mode values",
|
||||
toComplete: "mode=",
|
||||
expected: []string{"mode=replicated", "mode=global", "mode=replicated-job", "mode=global-job"},
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "name values",
|
||||
client: &fakeClient{
|
||||
serviceListFunc: func(_ context.Context, _ client.ServiceListOptions) (client.ServiceListResult, error) {
|
||||
return client.ServiceListResult{
|
||||
Items: []swarm.Service{
|
||||
*builders.Service(builders.ServiceName("s1")),
|
||||
*builders.Service(builders.ServiceName("s2")),
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
},
|
||||
toComplete: "name=",
|
||||
expected: []string{"name=s1", "name=s2"},
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "name values on API error",
|
||||
client: &fakeClient{
|
||||
serviceListFunc: func(_ context.Context, _ client.ServiceListOptions) (client.ServiceListResult, error) {
|
||||
return client.ServiceListResult{}, errors.New("API error")
|
||||
},
|
||||
},
|
||||
toComplete: "name=",
|
||||
expected: []string{},
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "label offers no values",
|
||||
toComplete: "label=",
|
||||
expected: nil,
|
||||
directive: cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
{
|
||||
doc: "unknown key falls back to the filter keys",
|
||||
toComplete: "bogus=",
|
||||
expected: []string{"id=", "label=", "mode=", "name="},
|
||||
directive: cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
cli := test.NewFakeCli(tc.client)
|
||||
completions, directive := completeServiceListFilters(cli)(newListCommand(cli), nil, tc.toComplete)
|
||||
assert.DeepEqual(t, completions, tc.expected)
|
||||
assert.Equal(t, directive, tc.directive)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ 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")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("filter", completeServiceListFilters(dockerCLI))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ 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")
|
||||
|
||||
_ = cmd.RegisterFlagCompletionFunc("filter", completeServicePsFilters(dockerCLI))
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user