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