1package main
2
3import (
4	"fmt"
5	"os"
6	"os/exec"
7	"strings"
8	"syscall"
9
10	"github.com/docker/cli/cli"
11	pluginmanager "github.com/docker/cli/cli-plugins/manager"
12	"github.com/docker/cli/cli/command"
13	"github.com/docker/cli/cli/command/commands"
14	cliflags "github.com/docker/cli/cli/flags"
15	"github.com/docker/cli/cli/version"
16	"github.com/docker/docker/api/types/versions"
17	"github.com/docker/docker/client"
18	"github.com/moby/buildkit/util/appcontext"
19	"github.com/pkg/errors"
20	"github.com/sirupsen/logrus"
21	"github.com/spf13/cobra"
22	"github.com/spf13/pflag"
23)
24
25var allowedAliases = map[string]struct{}{
26	"builder": {},
27}
28
29func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand {
30	var (
31		opts    *cliflags.ClientOptions
32		flags   *pflag.FlagSet
33		helpCmd *cobra.Command
34	)
35
36	cmd := &cobra.Command{
37		Use:              "docker [OPTIONS] COMMAND [ARG...]",
38		Short:            "A self-sufficient runtime for containers",
39		SilenceUsage:     true,
40		SilenceErrors:    true,
41		TraverseChildren: true,
42		RunE: func(cmd *cobra.Command, args []string) error {
43			if len(args) == 0 {
44				return command.ShowHelp(dockerCli.Err())(cmd, args)
45			}
46			return fmt.Errorf("docker: '%s' is not a docker command.\nSee 'docker --help'", args[0])
47
48		},
49		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
50			return isSupported(cmd, dockerCli)
51		},
52		Version:               fmt.Sprintf("%s, build %s", version.Version, version.GitCommit),
53		DisableFlagsInUseLine: true,
54	}
55	opts, flags, helpCmd = cli.SetupRootCommand(cmd)
56	flags.BoolP("version", "v", false, "Print version information and quit")
57
58	setFlagErrorFunc(dockerCli, cmd)
59
60	setupHelpCommand(dockerCli, cmd, helpCmd)
61	setHelpFunc(dockerCli, cmd)
62
63	cmd.SetOut(dockerCli.Out())
64	commands.AddCommands(cmd, dockerCli)
65
66	cli.DisableFlagsInUseLine(cmd)
67	setValidateArgs(dockerCli, cmd)
68
69	// flags must be the top-level command flags, not cmd.Flags()
70	return cli.NewTopLevelCommand(cmd, dockerCli, opts, flags)
71}
72
73func setFlagErrorFunc(dockerCli command.Cli, cmd *cobra.Command) {
74	// When invoking `docker stack --nonsense`, we need to make sure FlagErrorFunc return appropriate
75	// output if the feature is not supported.
76	// As above cli.SetupRootCommand(cmd) have already setup the FlagErrorFunc, we will add a pre-check before the FlagErrorFunc
77	// is called.
78	flagErrorFunc := cmd.FlagErrorFunc()
79	cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
80		if err := pluginmanager.AddPluginCommandStubs(dockerCli, cmd.Root()); err != nil {
81			return err
82		}
83		if err := isSupported(cmd, dockerCli); err != nil {
84			return err
85		}
86		if err := hideUnsupportedFeatures(cmd, dockerCli); err != nil {
87			return err
88		}
89		return flagErrorFunc(cmd, err)
90	})
91}
92
93func setupHelpCommand(dockerCli command.Cli, rootCmd, helpCmd *cobra.Command) {
94	origRun := helpCmd.Run
95	origRunE := helpCmd.RunE
96
97	helpCmd.Run = nil
98	helpCmd.RunE = func(c *cobra.Command, args []string) error {
99		if len(args) > 0 {
100			helpcmd, err := pluginmanager.PluginRunCommand(dockerCli, args[0], rootCmd)
101			if err == nil {
102				err = helpcmd.Run()
103				if err != nil {
104					return err
105				}
106			}
107			if !pluginmanager.IsNotFound(err) {
108				return err
109			}
110		}
111		if origRunE != nil {
112			return origRunE(c, args)
113		}
114		origRun(c, args)
115		return nil
116	}
117}
118
119func tryRunPluginHelp(dockerCli command.Cli, ccmd *cobra.Command, cargs []string) error {
120	root := ccmd.Root()
121
122	cmd, _, err := root.Traverse(cargs)
123	if err != nil {
124		return err
125	}
126	helpcmd, err := pluginmanager.PluginRunCommand(dockerCli, cmd.Name(), root)
127	if err != nil {
128		return err
129	}
130	return helpcmd.Run()
131}
132
133func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) {
134	defaultHelpFunc := cmd.HelpFunc()
135	cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) {
136		// Add a stub entry for every plugin so they are
137		// included in the help output and so that
138		// `tryRunPluginHelp` can find them or if we fall
139		// through they will be included in the default help
140		// output.
141		if err := pluginmanager.AddPluginCommandStubs(dockerCli, ccmd.Root()); err != nil {
142			ccmd.Println(err)
143			return
144		}
145
146		if len(args) >= 1 {
147			err := tryRunPluginHelp(dockerCli, ccmd, args)
148			if err == nil { // Successfully ran the plugin
149				return
150			}
151			if !pluginmanager.IsNotFound(err) {
152				ccmd.Println(err)
153				return
154			}
155		}
156
157		if err := isSupported(ccmd, dockerCli); err != nil {
158			ccmd.Println(err)
159			return
160		}
161		if err := hideUnsupportedFeatures(ccmd, dockerCli); err != nil {
162			ccmd.Println(err)
163			return
164		}
165
166		defaultHelpFunc(ccmd, args)
167	})
168}
169
170func setValidateArgs(dockerCli *command.DockerCli, cmd *cobra.Command) {
171	// The Args is handled by ValidateArgs in cobra, which does not allows a pre-hook.
172	// As a result, here we replace the existing Args validation func to a wrapper,
173	// where the wrapper will check to see if the feature is supported or not.
174	// The Args validation error will only be returned if the feature is supported.
175	cli.VisitAll(cmd, func(ccmd *cobra.Command) {
176		// if there is no tags for a command or any of its parent,
177		// there is no need to wrap the Args validation.
178		if !hasTags(ccmd) {
179			return
180		}
181
182		if ccmd.Args == nil {
183			return
184		}
185
186		cmdArgs := ccmd.Args
187		ccmd.Args = func(cmd *cobra.Command, args []string) error {
188			if err := isSupported(cmd, dockerCli); err != nil {
189				return err
190			}
191			return cmdArgs(cmd, args)
192		}
193	})
194}
195
196func tryPluginRun(dockerCli command.Cli, cmd *cobra.Command, subcommand string) error {
197	plugincmd, err := pluginmanager.PluginRunCommand(dockerCli, subcommand, cmd)
198	if err != nil {
199		return err
200	}
201
202	go func() {
203		// override SIGTERM handler so we let the plugin shut down first
204		<-appcontext.Context().Done()
205	}()
206
207	if err := plugincmd.Run(); err != nil {
208		statusCode := 1
209		exitErr, ok := err.(*exec.ExitError)
210		if !ok {
211			return err
212		}
213		if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok {
214			statusCode = ws.ExitStatus()
215		}
216		return cli.StatusError{
217			StatusCode: statusCode,
218		}
219	}
220	return nil
221}
222
223func processAliases(dockerCli command.Cli, cmd *cobra.Command, args, osArgs []string) ([]string, []string, error) {
224	aliasMap := dockerCli.ConfigFile().Aliases
225	aliases := make([][2][]string, 0, len(aliasMap))
226
227	for k, v := range aliasMap {
228		if _, ok := allowedAliases[k]; !ok {
229			return args, osArgs, errors.Errorf("Not allowed to alias %q. Allowed aliases: %#v", k, allowedAliases)
230		}
231		if _, _, err := cmd.Find(strings.Split(v, " ")); err == nil {
232			return args, osArgs, errors.Errorf("Not allowed to alias with builtin %q as target", v)
233		}
234		aliases = append(aliases, [2][]string{{k}, {v}})
235	}
236
237	if v, ok := aliasMap["builder"]; ok {
238		aliases = append(aliases,
239			[2][]string{{"build"}, {v, "build"}},
240			[2][]string{{"image", "build"}, {v, "build"}},
241		)
242	}
243	for _, al := range aliases {
244		var didChange bool
245		args, didChange = command.StringSliceReplaceAt(args, al[0], al[1], 0)
246		if didChange {
247			osArgs, _ = command.StringSliceReplaceAt(osArgs, al[0], al[1], -1)
248			break
249		}
250	}
251
252	return args, osArgs, nil
253}
254
255func runDocker(dockerCli *command.DockerCli) error {
256	tcmd := newDockerCommand(dockerCli)
257
258	cmd, args, err := tcmd.HandleGlobalFlags()
259	if err != nil {
260		return err
261	}
262
263	if err := tcmd.Initialize(); err != nil {
264		return err
265	}
266
267	args, os.Args, err = processAliases(dockerCli, cmd, args, os.Args)
268	if err != nil {
269		return err
270	}
271
272	if len(args) > 0 {
273		if _, _, err := cmd.Find(args); err != nil {
274			err := tryPluginRun(dockerCli, cmd, args[0])
275			if !pluginmanager.IsNotFound(err) {
276				return err
277			}
278			// For plugin not found we fall through to
279			// cmd.Execute() which deals with reporting
280			// "command not found" in a consistent way.
281		}
282	}
283
284	// We've parsed global args already, so reset args to those
285	// which remain.
286	cmd.SetArgs(args)
287	return cmd.Execute()
288}
289
290func main() {
291	dockerCli, err := command.NewDockerCli()
292	if err != nil {
293		fmt.Fprintln(os.Stderr, err)
294		os.Exit(1)
295	}
296	logrus.SetOutput(dockerCli.Err())
297
298	if err := runDocker(dockerCli); err != nil {
299		if sterr, ok := err.(cli.StatusError); ok {
300			if sterr.Status != "" {
301				fmt.Fprintln(dockerCli.Err(), sterr.Status)
302			}
303			// StatusError should only be used for errors, and all errors should
304			// have a non-zero exit status, so never exit with 0
305			if sterr.StatusCode == 0 {
306				os.Exit(1)
307			}
308			os.Exit(sterr.StatusCode)
309		}
310		fmt.Fprintln(dockerCli.Err(), err)
311		os.Exit(1)
312	}
313}
314
315type versionDetails interface {
316	Client() client.APIClient
317	ClientInfo() command.ClientInfo
318	ServerInfo() command.ServerInfo
319}
320
321func hideFlagIf(f *pflag.Flag, condition func(string) bool, annotation string) {
322	if f.Hidden {
323		return
324	}
325	var val string
326	if values, ok := f.Annotations[annotation]; ok {
327		if len(values) > 0 {
328			val = values[0]
329		}
330		if condition(val) {
331			f.Hidden = true
332		}
333	}
334}
335
336func hideSubcommandIf(subcmd *cobra.Command, condition func(string) bool, annotation string) {
337	if subcmd.Hidden {
338		return
339	}
340	if v, ok := subcmd.Annotations[annotation]; ok {
341		if condition(v) {
342			subcmd.Hidden = true
343		}
344	}
345}
346
347func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) error {
348	var (
349		buildKitDisabled = func(_ string) bool { v, _ := command.BuildKitEnabled(details.ServerInfo()); return !v }
350		buildKitEnabled  = func(_ string) bool { v, _ := command.BuildKitEnabled(details.ServerInfo()); return v }
351		notExperimental  = func(_ string) bool { return !details.ServerInfo().HasExperimental }
352		notOSType        = func(v string) bool { return v != details.ServerInfo().OSType }
353		versionOlderThan = func(v string) bool { return versions.LessThan(details.Client().ClientVersion(), v) }
354	)
355
356	cmd.Flags().VisitAll(func(f *pflag.Flag) {
357		// hide flags not supported by the server
358		// root command shows all top-level flags
359		if cmd.Parent() != nil {
360			if cmds, ok := f.Annotations["top-level"]; ok {
361				f.Hidden = !findCommand(cmd, cmds)
362			}
363			if f.Hidden {
364				return
365			}
366		}
367
368		hideFlagIf(f, buildKitDisabled, "buildkit")
369		hideFlagIf(f, buildKitEnabled, "no-buildkit")
370		hideFlagIf(f, notExperimental, "experimental")
371		hideFlagIf(f, notOSType, "ostype")
372		hideFlagIf(f, versionOlderThan, "version")
373	})
374
375	for _, subcmd := range cmd.Commands() {
376		hideSubcommandIf(subcmd, buildKitDisabled, "buildkit")
377		hideSubcommandIf(subcmd, buildKitEnabled, "no-buildkit")
378		hideSubcommandIf(subcmd, notExperimental, "experimental")
379		hideSubcommandIf(subcmd, notOSType, "ostype")
380		hideSubcommandIf(subcmd, versionOlderThan, "version")
381	}
382	return nil
383}
384
385// Checks if a command or one of its ancestors is in the list
386func findCommand(cmd *cobra.Command, commands []string) bool {
387	if cmd == nil {
388		return false
389	}
390	for _, c := range commands {
391		if c == cmd.Name() {
392			return true
393		}
394	}
395	return findCommand(cmd.Parent(), commands)
396}
397
398func isSupported(cmd *cobra.Command, details versionDetails) error {
399	if err := areSubcommandsSupported(cmd, details); err != nil {
400		return err
401	}
402	return areFlagsSupported(cmd, details)
403}
404
405func areFlagsSupported(cmd *cobra.Command, details versionDetails) error {
406	errs := []string{}
407
408	cmd.Flags().VisitAll(func(f *pflag.Flag) {
409		if !f.Changed {
410			return
411		}
412		if !isVersionSupported(f, details.Client().ClientVersion()) {
413			errs = append(errs, fmt.Sprintf(`"--%s" requires API version %s, but the Docker daemon API version is %s`, f.Name, getFlagAnnotation(f, "version"), details.Client().ClientVersion()))
414			return
415		}
416		if !isOSTypeSupported(f, details.ServerInfo().OSType) {
417			errs = append(errs, fmt.Sprintf(
418				`"--%s" is only supported on a Docker daemon running on %s, but the Docker daemon is running on %s`,
419				f.Name,
420				getFlagAnnotation(f, "ostype"), details.ServerInfo().OSType),
421			)
422			return
423		}
424		if _, ok := f.Annotations["experimental"]; ok && !details.ServerInfo().HasExperimental {
425			errs = append(errs, fmt.Sprintf(`"--%s" is only supported on a Docker daemon with experimental features enabled`, f.Name))
426		}
427		// buildkit-specific flags are noop when buildkit is not enabled, so we do not add an error in that case
428	})
429	if len(errs) > 0 {
430		return errors.New(strings.Join(errs, "\n"))
431	}
432	return nil
433}
434
435// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
436func areSubcommandsSupported(cmd *cobra.Command, details versionDetails) error {
437	// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
438	for curr := cmd; curr != nil; curr = curr.Parent() {
439		if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(details.Client().ClientVersion(), cmdVersion) {
440			return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, details.Client().ClientVersion())
441		}
442		if os, ok := curr.Annotations["ostype"]; ok && os != details.ServerInfo().OSType {
443			return fmt.Errorf("%s is only supported on a Docker daemon running on %s, but the Docker daemon is running on %s", cmd.CommandPath(), os, details.ServerInfo().OSType)
444		}
445		if _, ok := curr.Annotations["experimental"]; ok && !details.ServerInfo().HasExperimental {
446			return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath())
447		}
448	}
449	return nil
450}
451
452func getFlagAnnotation(f *pflag.Flag, annotation string) string {
453	if value, ok := f.Annotations[annotation]; ok && len(value) == 1 {
454		return value[0]
455	}
456	return ""
457}
458
459func isVersionSupported(f *pflag.Flag, clientVersion string) bool {
460	if v := getFlagAnnotation(f, "version"); v != "" {
461		return versions.GreaterThanOrEqualTo(clientVersion, v)
462	}
463	return true
464}
465
466func isOSTypeSupported(f *pflag.Flag, osType string) bool {
467	if v := getFlagAnnotation(f, "ostype"); v != "" && osType != "" {
468		return osType == v
469	}
470	return true
471}
472
473// hasTags return true if any of the command's parents has tags
474func hasTags(cmd *cobra.Command) bool {
475	for curr := cmd; curr != nil; curr = curr.Parent() {
476		if len(curr.Annotations) > 0 {
477			return true
478		}
479	}
480
481	return false
482}
483