github.com/ali-iotechsys/cli@v20.10.0+incompatible/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.SetOut(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 hideFlagIf(f *pflag.Flag, condition func(string) bool, annotation string) {
   316  	if f.Hidden {
   317  		return
   318  	}
   319  	if values, ok := f.Annotations[annotation]; ok && len(values) > 0 {
   320  		if condition(values[0]) {
   321  			f.Hidden = true
   322  		}
   323  	}
   324  }
   325  
   326  func hideSubcommandIf(subcmd *cobra.Command, condition func(string) bool, annotation string) {
   327  	if subcmd.Hidden {
   328  		return
   329  	}
   330  	if v, ok := subcmd.Annotations[annotation]; ok {
   331  		if condition(v) {
   332  			subcmd.Hidden = true
   333  		}
   334  	}
   335  }
   336  
   337  func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) error {
   338  	var (
   339  		buildKitDisabled = func(_ string) bool { v, _ := command.BuildKitEnabled(details.ServerInfo()); return !v }
   340  		buildKitEnabled  = func(_ string) bool { v, _ := command.BuildKitEnabled(details.ServerInfo()); return v }
   341  		notExperimental  = func(_ string) bool { return !details.ServerInfo().HasExperimental }
   342  		notOSType        = func(v string) bool { return v != details.ServerInfo().OSType }
   343  		versionOlderThan = func(v string) bool { return versions.LessThan(details.Client().ClientVersion(), v) }
   344  	)
   345  
   346  	cmd.Flags().VisitAll(func(f *pflag.Flag) {
   347  		// hide flags not supported by the server
   348  		// root command shows all top-level flags
   349  		if cmd.Parent() != nil {
   350  			if cmds, ok := f.Annotations["top-level"]; ok {
   351  				f.Hidden = !findCommand(cmd, cmds)
   352  			}
   353  			if f.Hidden {
   354  				return
   355  			}
   356  		}
   357  
   358  		hideFlagIf(f, buildKitDisabled, "buildkit")
   359  		hideFlagIf(f, buildKitEnabled, "no-buildkit")
   360  		hideFlagIf(f, notExperimental, "experimental")
   361  		hideFlagIf(f, notOSType, "ostype")
   362  		hideFlagIf(f, versionOlderThan, "version")
   363  	})
   364  
   365  	for _, subcmd := range cmd.Commands() {
   366  		hideSubcommandIf(subcmd, buildKitDisabled, "buildkit")
   367  		hideSubcommandIf(subcmd, buildKitEnabled, "no-buildkit")
   368  		hideSubcommandIf(subcmd, notExperimental, "experimental")
   369  		hideSubcommandIf(subcmd, notOSType, "ostype")
   370  		hideSubcommandIf(subcmd, versionOlderThan, "version")
   371  	}
   372  	return nil
   373  }
   374  
   375  // Checks if a command or one of its ancestors is in the list
   376  func findCommand(cmd *cobra.Command, commands []string) bool {
   377  	if cmd == nil {
   378  		return false
   379  	}
   380  	for _, c := range commands {
   381  		if c == cmd.Name() {
   382  			return true
   383  		}
   384  	}
   385  	return findCommand(cmd.Parent(), commands)
   386  }
   387  
   388  func isSupported(cmd *cobra.Command, details versionDetails) error {
   389  	if err := areSubcommandsSupported(cmd, details); err != nil {
   390  		return err
   391  	}
   392  	return areFlagsSupported(cmd, details)
   393  }
   394  
   395  func areFlagsSupported(cmd *cobra.Command, details versionDetails) error {
   396  	errs := []string{}
   397  
   398  	cmd.Flags().VisitAll(func(f *pflag.Flag) {
   399  		if !f.Changed {
   400  			return
   401  		}
   402  		if !isVersionSupported(f, details.Client().ClientVersion()) {
   403  			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()))
   404  			return
   405  		}
   406  		if !isOSTypeSupported(f, details.ServerInfo().OSType) {
   407  			errs = append(errs, fmt.Sprintf(
   408  				`"--%s" is only supported on a Docker daemon running on %s, but the Docker daemon is running on %s`,
   409  				f.Name,
   410  				getFlagAnnotation(f, "ostype"), details.ServerInfo().OSType),
   411  			)
   412  			return
   413  		}
   414  		if _, ok := f.Annotations["experimental"]; ok && !details.ServerInfo().HasExperimental {
   415  			errs = append(errs, fmt.Sprintf(`"--%s" is only supported on a Docker daemon with experimental features enabled`, f.Name))
   416  		}
   417  		// buildkit-specific flags are noop when buildkit is not enabled, so we do not add an error in that case
   418  	})
   419  	if len(errs) > 0 {
   420  		return errors.New(strings.Join(errs, "\n"))
   421  	}
   422  	return nil
   423  }
   424  
   425  // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
   426  func areSubcommandsSupported(cmd *cobra.Command, details versionDetails) error {
   427  	// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
   428  	for curr := cmd; curr != nil; curr = curr.Parent() {
   429  		if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(details.Client().ClientVersion(), cmdVersion) {
   430  			return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, details.Client().ClientVersion())
   431  		}
   432  		if os, ok := curr.Annotations["ostype"]; ok && os != details.ServerInfo().OSType {
   433  			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)
   434  		}
   435  		if _, ok := curr.Annotations["experimental"]; ok && !details.ServerInfo().HasExperimental {
   436  			return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath())
   437  		}
   438  	}
   439  	return nil
   440  }
   441  
   442  func getFlagAnnotation(f *pflag.Flag, annotation string) string {
   443  	if value, ok := f.Annotations[annotation]; ok && len(value) == 1 {
   444  		return value[0]
   445  	}
   446  	return ""
   447  }
   448  
   449  func isVersionSupported(f *pflag.Flag, clientVersion string) bool {
   450  	if v := getFlagAnnotation(f, "version"); v != "" {
   451  		return versions.GreaterThanOrEqualTo(clientVersion, v)
   452  	}
   453  	return true
   454  }
   455  
   456  func isOSTypeSupported(f *pflag.Flag, osType string) bool {
   457  	if v := getFlagAnnotation(f, "ostype"); v != "" && osType != "" {
   458  		return osType == v
   459  	}
   460  	return true
   461  }
   462  
   463  // hasTags return true if any of the command's parents has tags
   464  func hasTags(cmd *cobra.Command) bool {
   465  	for curr := cmd; curr != nil; curr = curr.Parent() {
   466  		if len(curr.Annotations) > 0 {
   467  			return true
   468  		}
   469  	}
   470  
   471  	return false
   472  }