github.com/pwn-term/docker@v0.0.0-20210616085119-6e977cce2565/cli/cmd/docker/docker.go (about)

     1  package main
     2  
     3  import (
     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  
    25  var allowedAliases = map[string]struct{}{
    26  	"builder": {},
    27  }
    28  
    29  func 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  
    73  func 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  
    93  func 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  
   119  func 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  
   133  func 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  
   170  func 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  
   196  func 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  
   223  func 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  
   255  func 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  
   290  func 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  
   315  type versionDetails interface {
   316  	Client() client.APIClient
   317  	ClientInfo() command.ClientInfo
   318  	ServerInfo() command.ServerInfo
   319  }
   320  
   321  func 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  
   336  func 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  
   347  func 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
   386  func 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  
   398  func 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  
   405  func 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`
   436  func 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  
   452  func 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  
   459  func 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  
   466  func 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
   474  func 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  }