Skip to content

Commit 4adbb18

Browse files
authored
Merge pull request #5580 from albers/container-completions
Improve Cobra completions for `run` and `create`
2 parents e00ed82 + 06260e6 commit 4adbb18

File tree

4 files changed

+377
-20
lines changed

4 files changed

+377
-20
lines changed

cli/command/container/completion.go

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,65 @@ var allLinuxCapabilities = sync.OnceValue(func() []string {
4444
return out
4545
})
4646

47+
// logDriverOptions provides the options for each built-in logging driver.
48+
var logDriverOptions = map[string][]string{
49+
"awslogs": {
50+
"max-buffer-size", "mode", "awslogs-create-group", "awslogs-credentials-endpoint", "awslogs-datetime-format",
51+
"awslogs-group", "awslogs-multiline-pattern", "awslogs-region", "awslogs-stream", "tag",
52+
},
53+
"fluentd": {
54+
"max-buffer-size", "mode", "env", "env-regex", "labels", "fluentd-address", "fluentd-async",
55+
"fluentd-buffer-limit", "fluentd-request-ack", "fluentd-retry-wait", "fluentd-max-retries",
56+
"fluentd-sub-second-precision", "tag",
57+
},
58+
"gcplogs": {
59+
"max-buffer-size", "mode", "env", "env-regex", "labels", "gcp-log-cmd", "gcp-meta-id", "gcp-meta-name",
60+
"gcp-meta-zone", "gcp-project",
61+
},
62+
"gelf": {
63+
"max-buffer-size", "mode", "env", "env-regex", "labels", "gelf-address", "gelf-compression-level",
64+
"gelf-compression-type", "gelf-tcp-max-reconnect", "gelf-tcp-reconnect-delay", "tag",
65+
},
66+
"journald": {"max-buffer-size", "mode", "env", "env-regex", "labels", "tag"},
67+
"json-file": {"max-buffer-size", "mode", "env", "env-regex", "labels", "compress", "max-file", "max-size"},
68+
"local": {"max-buffer-size", "mode", "compress", "max-file", "max-size"},
69+
"none": {},
70+
"splunk": {
71+
"max-buffer-size", "mode", "env", "env-regex", "labels", "splunk-caname", "splunk-capath", "splunk-format",
72+
"splunk-gzip", "splunk-gzip-level", "splunk-index", "splunk-insecureskipverify", "splunk-source",
73+
"splunk-sourcetype", "splunk-token", "splunk-url", "splunk-verify-connection", "tag",
74+
},
75+
"syslog": {
76+
"max-buffer-size", "mode", "env", "env-regex", "labels", "syslog-address", "syslog-facility", "syslog-format",
77+
"syslog-tls-ca-cert", "syslog-tls-cert", "syslog-tls-key", "syslog-tls-skip-verify", "tag",
78+
},
79+
}
80+
81+
// builtInLogDrivers provides a list of the built-in logging drivers.
82+
var builtInLogDrivers = sync.OnceValue(func() []string {
83+
drivers := make([]string, 0, len(logDriverOptions))
84+
for driver := range logDriverOptions {
85+
drivers = append(drivers, driver)
86+
}
87+
return drivers
88+
})
89+
90+
// allLogDriverOptions provides all options of the built-in logging drivers.
91+
// The list does not contain duplicates.
92+
var allLogDriverOptions = sync.OnceValue(func() []string {
93+
var result []string
94+
seen := make(map[string]bool)
95+
for driver := range logDriverOptions {
96+
for _, opt := range logDriverOptions[driver] {
97+
if !seen[opt] {
98+
seen[opt] = true
99+
result = append(result, opt)
100+
}
101+
}
102+
}
103+
return result
104+
})
105+
47106
// restartPolicies is a list of all valid restart-policies..
48107
//
49108
// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
@@ -54,6 +113,207 @@ var restartPolicies = []string{
54113
string(container.RestartPolicyUnlessStopped),
55114
}
56115

116+
// addCompletions adds the completions that `run` and `create` have in common.
117+
func addCompletions(cmd *cobra.Command, dockerCLI completion.APIClientProvider) {
118+
_ = cmd.RegisterFlagCompletionFunc("attach", completion.FromList("stderr", "stdin", "stdout"))
119+
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
120+
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
121+
_ = cmd.RegisterFlagCompletionFunc("cgroupns", completeCgroupns())
122+
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
123+
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
124+
_ = cmd.RegisterFlagCompletionFunc("ipc", completeIpc(dockerCLI))
125+
_ = cmd.RegisterFlagCompletionFunc("link", completeLink(dockerCLI))
126+
_ = cmd.RegisterFlagCompletionFunc("log-driver", completeLogDriver(dockerCLI))
127+
_ = cmd.RegisterFlagCompletionFunc("log-opt", completeLogOpt)
128+
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCLI))
129+
_ = cmd.RegisterFlagCompletionFunc("pid", completePid(dockerCLI))
130+
_ = cmd.RegisterFlagCompletionFunc("platform", completion.Platforms)
131+
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
132+
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
133+
_ = cmd.RegisterFlagCompletionFunc("security-opt", completeSecurityOpt)
134+
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
135+
_ = cmd.RegisterFlagCompletionFunc("storage-opt", completeStorageOpt)
136+
_ = cmd.RegisterFlagCompletionFunc("ulimit", completeUlimit)
137+
_ = cmd.RegisterFlagCompletionFunc("userns", completion.FromList("host"))
138+
_ = cmd.RegisterFlagCompletionFunc("uts", completion.FromList("host"))
139+
_ = cmd.RegisterFlagCompletionFunc("volume-driver", completeVolumeDriver(dockerCLI))
140+
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCLI, true))
141+
}
142+
143+
// completeCgroupns implements shell completion for the `--cgroupns` option of `run` and `create`.
144+
func completeCgroupns() completion.ValidArgsFn {
145+
return completion.FromList(string(container.CgroupnsModeHost), string(container.CgroupnsModePrivate))
146+
}
147+
148+
// completeDetachKeys implements shell completion for the `--detach-keys` option of `run` and `create`.
149+
func completeDetachKeys(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
150+
return []string{"ctrl-"}, cobra.ShellCompDirectiveNoSpace
151+
}
152+
153+
// completeIpc implements shell completion for the `--ipc` option of `run` and `create`.
154+
// The completion is partly composite.
155+
func completeIpc(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
156+
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
157+
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
158+
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
159+
}
160+
if strings.HasPrefix(toComplete, "container:") {
161+
names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete)
162+
return prefixWith("container:", names), cobra.ShellCompDirectiveNoFileComp
163+
}
164+
return []string{
165+
string(container.IPCModeContainer + ":"),
166+
string(container.IPCModeHost),
167+
string(container.IPCModeNone),
168+
string(container.IPCModePrivate),
169+
string(container.IPCModeShareable),
170+
}, cobra.ShellCompDirectiveNoFileComp
171+
}
172+
}
173+
174+
// completeLink implements shell completion for the `--link` option of `run` and `create`.
175+
func completeLink(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
176+
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
177+
return postfixWith(":", containerNames(dockerCLI, cmd, args, toComplete)), cobra.ShellCompDirectiveNoSpace
178+
}
179+
}
180+
181+
// completeLogDriver implements shell completion for the `--log-driver` option of `run` and `create`.
182+
// The log drivers are collected from a call to the Info endpoint with a fallback to a hard-coded list
183+
// of the build-in log drivers.
184+
func completeLogDriver(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
185+
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
186+
info, err := dockerCLI.Client().Info(cmd.Context())
187+
if err != nil {
188+
return builtInLogDrivers(), cobra.ShellCompDirectiveNoFileComp
189+
}
190+
drivers := info.Plugins.Log
191+
return drivers, cobra.ShellCompDirectiveNoFileComp
192+
}
193+
}
194+
195+
// completeLogOpt implements shell completion for the `--log-opt` option of `run` and `create`.
196+
// If the user supplied a log-driver, only options for that driver are returned.
197+
func completeLogOpt(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
198+
driver, _ := cmd.Flags().GetString("log-driver")
199+
if options, exists := logDriverOptions[driver]; exists {
200+
return postfixWith("=", options), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
201+
}
202+
return postfixWith("=", allLogDriverOptions()), cobra.ShellCompDirectiveNoSpace
203+
}
204+
205+
// completePid implements shell completion for the `--pid` option of `run` and `create`.
206+
func completePid(dockerCLI completion.APIClientProvider) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
207+
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
208+
if len(toComplete) > 0 && strings.HasPrefix("container", toComplete) { //nolint:gocritic // not swapped, matches partly typed "container"
209+
return []string{"container:"}, cobra.ShellCompDirectiveNoSpace
210+
}
211+
if strings.HasPrefix(toComplete, "container:") {
212+
names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete)
213+
return prefixWith("container:", names), cobra.ShellCompDirectiveNoFileComp
214+
}
215+
return []string{"container:", "host"}, cobra.ShellCompDirectiveNoFileComp
216+
}
217+
}
218+
219+
// completeSecurityOpt implements shell completion for the `--security-opt` option of `run` and `create`.
220+
// The completion is partly composite.
221+
func completeSecurityOpt(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {
222+
if len(toComplete) > 0 && strings.HasPrefix("apparmor=", toComplete) { //nolint:gocritic // not swapped, matches partly typed "apparmor="
223+
return []string{"apparmor="}, cobra.ShellCompDirectiveNoSpace
224+
}
225+
if len(toComplete) > 0 && strings.HasPrefix("label", toComplete) { //nolint:gocritic // not swapped, matches partly typed "label"
226+
return []string{"label="}, cobra.ShellCompDirectiveNoSpace
227+
}
228+
if strings.HasPrefix(toComplete, "label=") {
229+
if strings.HasPrefix(toComplete, "label=d") {
230+
return []string{"label=disable"}, cobra.ShellCompDirectiveNoFileComp
231+
}
232+
labels := []string{"disable", "level:", "role:", "type:", "user:"}
233+
return prefixWith("label=", labels), cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
234+
}
235+
// length must be > 1 here so that completion of "s" falls through.
236+
if len(toComplete) > 1 && strings.HasPrefix("seccomp", toComplete) { //nolint:gocritic // not swapped, matches partly typed "seccomp"
237+
return []string{"seccomp="}, cobra.ShellCompDirectiveNoSpace
238+
}
239+
if strings.HasPrefix(toComplete, "seccomp=") {
240+
return []string{"seccomp=unconfined"}, cobra.ShellCompDirectiveNoFileComp
241+
}
242+
return []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"}, cobra.ShellCompDirectiveNoFileComp
243+
}
244+
245+
// completeStorageOpt implements shell completion for the `--storage-opt` option of `run` and `create`.
246+
func completeStorageOpt(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
247+
return []string{"size="}, cobra.ShellCompDirectiveNoSpace
248+
}
249+
250+
// completeUlimit implements shell completion for the `--ulimit` option of `run` and `create`.
251+
func completeUlimit(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
252+
limits := []string{
253+
"as",
254+
"chroot",
255+
"core",
256+
"cpu",
257+
"data",
258+
"fsize",
259+
"locks",
260+
"maxlogins",
261+
"maxsyslogins",
262+
"memlock",
263+
"msgqueue",
264+
"nice",
265+
"nofile",
266+
"nproc",
267+
"priority",
268+
"rss",
269+
"rtprio",
270+
"sigpending",
271+
"stack",
272+
}
273+
return postfixWith("=", limits), cobra.ShellCompDirectiveNoSpace
274+
}
275+
276+
// completeVolumeDriver contacts the API to get the built-in and installed volume drivers.
277+
func completeVolumeDriver(dockerCLI completion.APIClientProvider) completion.ValidArgsFn {
278+
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
279+
info, err := dockerCLI.Client().Info(cmd.Context())
280+
if err != nil {
281+
// fallback: the built-in drivers
282+
return []string{"local"}, cobra.ShellCompDirectiveNoFileComp
283+
}
284+
drivers := info.Plugins.Volume
285+
return drivers, cobra.ShellCompDirectiveNoFileComp
286+
}
287+
}
288+
289+
// containerNames contacts the API to get names and optionally IDs of containers.
290+
// In case of an error, an empty list is returned.
291+
func containerNames(dockerCLI completion.APIClientProvider, cmd *cobra.Command, args []string, toComplete string) []string {
292+
names, _ := completion.ContainerNames(dockerCLI, true)(cmd, args, toComplete)
293+
if names == nil {
294+
return []string{}
295+
}
296+
return names
297+
}
298+
299+
// prefixWith prefixes every element in the slice with the given prefix.
300+
func prefixWith(prefix string, values []string) []string {
301+
result := make([]string, len(values))
302+
for i, v := range values {
303+
result[i] = prefix + v
304+
}
305+
return result
306+
}
307+
308+
// postfixWith appends postfix to every element in the slice.
309+
func postfixWith(postfix string, values []string) []string {
310+
result := make([]string, len(values))
311+
for i, v := range values {
312+
result[i] = v + postfix
313+
}
314+
return result
315+
}
316+
57317
func completeLinuxCapabilityNames(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
58318
return completion.FromList(allLinuxCapabilities()...)(cmd, args, toComplete)
59319
}

cli/command/container/completion_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"strings"
55
"testing"
66

7+
"github.com/docker/cli/internal/test"
8+
"github.com/docker/cli/internal/test/builders"
9+
"github.com/docker/docker/api/types/container"
710
"github.com/moby/sys/signal"
811
"github.com/spf13/cobra"
912
"gotest.tools/v3/assert"
@@ -21,13 +24,108 @@ func TestCompleteLinuxCapabilityNames(t *testing.T) {
2124
}
2225
}
2326

27+
func TestCompletePid(t *testing.T) {
28+
tests := []struct {
29+
containerListFunc func(container.ListOptions) ([]container.Summary, error)
30+
toComplete string
31+
expectedCompletions []string
32+
expectedDirective cobra.ShellCompDirective
33+
}{
34+
{
35+
toComplete: "",
36+
expectedCompletions: []string{"container:", "host"},
37+
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
38+
},
39+
{
40+
toComplete: "c",
41+
expectedCompletions: []string{"container:"},
42+
expectedDirective: cobra.ShellCompDirectiveNoSpace,
43+
},
44+
{
45+
containerListFunc: func(container.ListOptions) ([]container.Summary, error) {
46+
return []container.Summary{
47+
*builders.Container("c1"),
48+
*builders.Container("c2"),
49+
}, nil
50+
},
51+
toComplete: "container:",
52+
expectedCompletions: []string{"container:c1", "container:c2"},
53+
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
54+
},
55+
}
56+
57+
for _, tc := range tests {
58+
t.Run(tc.toComplete, func(t *testing.T) {
59+
cli := test.NewFakeCli(&fakeClient{
60+
containerListFunc: tc.containerListFunc,
61+
})
62+
completions, directive := completePid(cli)(NewRunCommand(cli), nil, tc.toComplete)
63+
assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions))
64+
assert.Check(t, is.Equal(directive, tc.expectedDirective))
65+
})
66+
}
67+
}
68+
2469
func TestCompleteRestartPolicies(t *testing.T) {
2570
values, directives := completeRestartPolicies(nil, nil, "")
2671
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")
2772
expected := restartPolicies
2873
assert.Check(t, is.DeepEqual(values, expected))
2974
}
3075

76+
func TestCompleteSecurityOpt(t *testing.T) {
77+
tests := []struct {
78+
toComplete string
79+
expectedCompletions []string
80+
expectedDirective cobra.ShellCompDirective
81+
}{
82+
{
83+
toComplete: "",
84+
expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"},
85+
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
86+
},
87+
{
88+
toComplete: "apparmor=",
89+
expectedCompletions: []string{"apparmor="},
90+
expectedDirective: cobra.ShellCompDirectiveNoSpace,
91+
},
92+
{
93+
toComplete: "label=",
94+
expectedCompletions: []string{"label=disable", "label=level:", "label=role:", "label=type:", "label=user:"},
95+
expectedDirective: cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp,
96+
},
97+
{
98+
toComplete: "s",
99+
// We do not filter matching completions but delegate this task to the shell script.
100+
expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"},
101+
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
102+
},
103+
{
104+
toComplete: "se",
105+
expectedCompletions: []string{"seccomp="},
106+
expectedDirective: cobra.ShellCompDirectiveNoSpace,
107+
},
108+
{
109+
toComplete: "seccomp=",
110+
expectedCompletions: []string{"seccomp=unconfined"},
111+
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
112+
},
113+
{
114+
toComplete: "sy",
115+
expectedCompletions: []string{"apparmor=", "label=", "no-new-privileges", "seccomp=", "systempaths=unconfined"},
116+
expectedDirective: cobra.ShellCompDirectiveNoFileComp,
117+
},
118+
}
119+
120+
for _, tc := range tests {
121+
t.Run(tc.toComplete, func(t *testing.T) {
122+
completions, directive := completeSecurityOpt(nil, nil, tc.toComplete)
123+
assert.Check(t, is.DeepEqual(completions, tc.expectedCompletions))
124+
assert.Check(t, is.Equal(directive, tc.expectedDirective))
125+
})
126+
}
127+
}
128+
31129
func TestCompleteSignals(t *testing.T) {
32130
values, directives := completeSignals(nil, nil, "")
33131
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp), "Should not perform file completion")

0 commit comments

Comments
 (0)