Merge pull request #7124 from JessThrysoee/completion-swarm-filters

completion: add completion for swarm "--filter" flags
This commit is contained in:
Sebastiaan van Stijn
2026-07-27 21:30:07 +02:00
committed by GitHub
9 changed files with 373 additions and 0 deletions
+22
View File
@@ -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.
+12
View File
@@ -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
+34
View File
@@ -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
}
}
}
+52
View File
@@ -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)
})
}
}
+2
View File
@@ -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
}
+91
View File
@@ -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
}
+156
View File
@@ -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)
})
}
}
+2
View File
@@ -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
}
+2
View File
@@ -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
}