github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/cmd/docker/docker.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"github.com/docker/cli/cli"
    11  	"github.com/docker/cli/cli/command"
    12  	"github.com/docker/cli/cli/command/commands"
    13  	cliconfig "github.com/docker/cli/cli/config"
    14  	"github.com/docker/cli/cli/debug"
    15  	cliflags "github.com/docker/cli/cli/flags"
    16  	"github.com/docker/cli/internal/containerizedengine"
    17  	"github.com/docker/docker/api/types/versions"
    18  	"github.com/docker/docker/client"
    19  	"github.com/docker/docker/pkg/term"
    20  	"github.com/sirupsen/logrus"
    21  	"github.com/spf13/cobra"
    22  	"github.com/spf13/pflag"
    23  )
    24  
    25  func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
    26  	opts := cliflags.NewClientOptions()
    27  	var flags *pflag.FlagSet
    28  
    29  	cmd := &cobra.Command{
    30  		Use:              "docker [OPTIONS] COMMAND [ARG...]",
    31  		Short:            "A self-sufficient runtime for containers",
    32  		SilenceUsage:     true,
    33  		SilenceErrors:    true,
    34  		TraverseChildren: true,
    35  		Args:             noArgs,
    36  		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
    37  			// flags must be the top-level command flags, not cmd.Flags()
    38  			opts.Common.SetDefaultOptions(flags)
    39  			dockerPreRun(opts)
    40  			if err := dockerCli.Initialize(opts); err != nil {
    41  				return err
    42  			}
    43  			return isSupported(cmd, dockerCli)
    44  		},
    45  		Version:               fmt.Sprintf("%s, build %s", cli.Version, cli.GitCommit),
    46  		DisableFlagsInUseLine: true,
    47  	}
    48  	cli.SetupRootCommand(cmd)
    49  
    50  	flags = cmd.Flags()
    51  	flags.BoolP("version", "v", false, "Print version information and quit")
    52  	flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files")
    53  	opts.Common.InstallFlags(flags)
    54  
    55  	setFlagErrorFunc(dockerCli, cmd, flags, opts)
    56  
    57  	setHelpFunc(dockerCli, cmd, flags, opts)
    58  
    59  	cmd.SetOutput(dockerCli.Out())
    60  	commands.AddCommands(cmd, dockerCli)
    61  
    62  	disableFlagsInUseLine(cmd)
    63  	setValidateArgs(dockerCli, cmd, flags, opts)
    64  
    65  	return cmd
    66  }
    67  
    68  func disableFlagsInUseLine(cmd *cobra.Command) {
    69  	visitAll(cmd, func(ccmd *cobra.Command) {
    70  		// do not add a `[flags]` to the end of the usage line.
    71  		ccmd.DisableFlagsInUseLine = true
    72  	})
    73  }
    74  
    75  func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
    76  	// When invoking `docker stack --nonsense`, we need to make sure FlagErrorFunc return appropriate
    77  	// output if the feature is not supported.
    78  	// As above cli.SetupRootCommand(cmd) have already setup the FlagErrorFunc, we will add a pre-check before the FlagErrorFunc
    79  	// is called.
    80  	flagErrorFunc := cmd.FlagErrorFunc()
    81  	cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
    82  		if err := initializeDockerCli(dockerCli, flags, opts); err != nil {
    83  			return err
    84  		}
    85  		if err := isSupported(cmd, dockerCli); err != nil {
    86  			return err
    87  		}
    88  		return flagErrorFunc(cmd, err)
    89  	})
    90  }
    91  
    92  func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
    93  	defaultHelpFunc := cmd.HelpFunc()
    94  	cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) {
    95  		if err := initializeDockerCli(dockerCli, flags, opts); err != nil {
    96  			ccmd.Println(err)
    97  			return
    98  		}
    99  		if err := isSupported(ccmd, dockerCli); err != nil {
   100  			ccmd.Println(err)
   101  			return
   102  		}
   103  
   104  		hideUnsupportedFeatures(ccmd, dockerCli)
   105  		defaultHelpFunc(ccmd, args)
   106  	})
   107  }
   108  
   109  func setValidateArgs(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) {
   110  	// The Args is handled by ValidateArgs in cobra, which does not allows a pre-hook.
   111  	// As a result, here we replace the existing Args validation func to a wrapper,
   112  	// where the wrapper will check to see if the feature is supported or not.
   113  	// The Args validation error will only be returned if the feature is supported.
   114  	visitAll(cmd, func(ccmd *cobra.Command) {
   115  		// if there is no tags for a command or any of its parent,
   116  		// there is no need to wrap the Args validation.
   117  		if !hasTags(ccmd) {
   118  			return
   119  		}
   120  
   121  		if ccmd.Args == nil {
   122  			return
   123  		}
   124  
   125  		cmdArgs := ccmd.Args
   126  		ccmd.Args = func(cmd *cobra.Command, args []string) error {
   127  			if err := initializeDockerCli(dockerCli, flags, opts); err != nil {
   128  				return err
   129  			}
   130  			if err := isSupported(cmd, dockerCli); err != nil {
   131  				return err
   132  			}
   133  			return cmdArgs(cmd, args)
   134  		}
   135  	})
   136  }
   137  
   138  func initializeDockerCli(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *cliflags.ClientOptions) error {
   139  	if dockerCli.Client() != nil {
   140  		return nil
   141  	}
   142  	// when using --help, PersistentPreRun is not called, so initialization is needed.
   143  	// flags must be the top-level command flags, not cmd.Flags()
   144  	opts.Common.SetDefaultOptions(flags)
   145  	dockerPreRun(opts)
   146  	return dockerCli.Initialize(opts)
   147  }
   148  
   149  // visitAll will traverse all commands from the root.
   150  // This is different from the VisitAll of cobra.Command where only parents
   151  // are checked.
   152  func visitAll(root *cobra.Command, fn func(*cobra.Command)) {
   153  	for _, cmd := range root.Commands() {
   154  		visitAll(cmd, fn)
   155  	}
   156  	fn(root)
   157  }
   158  
   159  func noArgs(cmd *cobra.Command, args []string) error {
   160  	if len(args) == 0 {
   161  		return nil
   162  	}
   163  	return fmt.Errorf(
   164  		"docker: '%s' is not a docker command.\nSee 'docker --help'", args[0])
   165  }
   166  
   167  func main() {
   168  	// Set terminal emulation based on platform as required.
   169  	stdin, stdout, stderr := term.StdStreams()
   170  	logrus.SetOutput(stderr)
   171  
   172  	dockerCli := command.NewDockerCli(stdin, stdout, stderr, contentTrustEnabled(), containerizedengine.NewClient)
   173  	cmd := newDockerCommand(dockerCli)
   174  
   175  	if err := cmd.Execute(); err != nil {
   176  		if sterr, ok := err.(cli.StatusError); ok {
   177  			if sterr.Status != "" {
   178  				fmt.Fprintln(stderr, sterr.Status)
   179  			}
   180  			// StatusError should only be used for errors, and all errors should
   181  			// have a non-zero exit status, so never exit with 0
   182  			if sterr.StatusCode == 0 {
   183  				os.Exit(1)
   184  			}
   185  			os.Exit(sterr.StatusCode)
   186  		}
   187  		fmt.Fprintln(stderr, err)
   188  		os.Exit(1)
   189  	}
   190  }
   191  
   192  func contentTrustEnabled() bool {
   193  	if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
   194  		if t, err := strconv.ParseBool(e); t || err != nil {
   195  			// treat any other value as true
   196  			return true
   197  		}
   198  	}
   199  	return false
   200  }
   201  
   202  func dockerPreRun(opts *cliflags.ClientOptions) {
   203  	cliflags.SetLogLevel(opts.Common.LogLevel)
   204  
   205  	if opts.ConfigDir != "" {
   206  		cliconfig.SetDir(opts.ConfigDir)
   207  	}
   208  
   209  	if opts.Common.Debug {
   210  		debug.Enable()
   211  	}
   212  }
   213  
   214  type versionDetails interface {
   215  	Client() client.APIClient
   216  	ClientInfo() command.ClientInfo
   217  	ServerInfo() command.ServerInfo
   218  }
   219  
   220  func hideFeatureFlag(f *pflag.Flag, hasFeature bool, annotation string) {
   221  	if hasFeature {
   222  		return
   223  	}
   224  	if _, ok := f.Annotations[annotation]; ok {
   225  		f.Hidden = true
   226  	}
   227  }
   228  
   229  func hideFeatureSubCommand(subcmd *cobra.Command, hasFeature bool, annotation string) {
   230  	if hasFeature {
   231  		return
   232  	}
   233  	if _, ok := subcmd.Annotations[annotation]; ok {
   234  		subcmd.Hidden = true
   235  	}
   236  }
   237  
   238  func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) {
   239  	clientVersion := details.Client().ClientVersion()
   240  	osType := details.ServerInfo().OSType
   241  	hasExperimental := details.ServerInfo().HasExperimental
   242  	hasExperimentalCLI := details.ClientInfo().HasExperimental
   243  
   244  	cmd.Flags().VisitAll(func(f *pflag.Flag) {
   245  		hideFeatureFlag(f, hasExperimental, "experimental")
   246  		hideFeatureFlag(f, hasExperimentalCLI, "experimentalCLI")
   247  		// hide flags not supported by the server
   248  		if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) {
   249  			f.Hidden = true
   250  		}
   251  		// root command shows all top-level flags
   252  		if cmd.Parent() != nil {
   253  			if commands, ok := f.Annotations["top-level"]; ok {
   254  				f.Hidden = !findCommand(cmd, commands)
   255  			}
   256  		}
   257  	})
   258  
   259  	for _, subcmd := range cmd.Commands() {
   260  		hideFeatureSubCommand(subcmd, hasExperimental, "experimental")
   261  		hideFeatureSubCommand(subcmd, hasExperimentalCLI, "experimentalCLI")
   262  		// hide subcommands not supported by the server
   263  		if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) {
   264  			subcmd.Hidden = true
   265  		}
   266  		if v, ok := subcmd.Annotations["ostype"]; ok && v != osType {
   267  			subcmd.Hidden = true
   268  		}
   269  	}
   270  }
   271  
   272  // Checks if a command or one of its ancestors is in the list
   273  func findCommand(cmd *cobra.Command, commands []string) bool {
   274  	if cmd == nil {
   275  		return false
   276  	}
   277  	for _, c := range commands {
   278  		if c == cmd.Name() {
   279  			return true
   280  		}
   281  	}
   282  	return findCommand(cmd.Parent(), commands)
   283  }
   284  
   285  func isSupported(cmd *cobra.Command, details versionDetails) error {
   286  	if err := areSubcommandsSupported(cmd, details); err != nil {
   287  		return err
   288  	}
   289  	return areFlagsSupported(cmd, details)
   290  }
   291  
   292  func areFlagsSupported(cmd *cobra.Command, details versionDetails) error {
   293  	clientVersion := details.Client().ClientVersion()
   294  	osType := details.ServerInfo().OSType
   295  	hasExperimental := details.ServerInfo().HasExperimental
   296  	hasExperimentalCLI := details.ClientInfo().HasExperimental
   297  
   298  	errs := []string{}
   299  
   300  	cmd.Flags().VisitAll(func(f *pflag.Flag) {
   301  		if f.Changed {
   302  			if !isVersionSupported(f, clientVersion) {
   303  				errs = append(errs, fmt.Sprintf("\"--%s\" requires API version %s, but the Docker daemon API version is %s", f.Name, getFlagAnnotation(f, "version"), clientVersion))
   304  				return
   305  			}
   306  			if !isOSTypeSupported(f, osType) {
   307  				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))
   308  				return
   309  			}
   310  			if _, ok := f.Annotations["experimental"]; ok && !hasExperimental {
   311  				errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker daemon with experimental features enabled", f.Name))
   312  			}
   313  			if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
   314  				errs = append(errs, fmt.Sprintf("\"--%s\" is on a Docker cli with experimental cli features enabled", f.Name))
   315  			}
   316  		}
   317  	})
   318  	if len(errs) > 0 {
   319  		return errors.New(strings.Join(errs, "\n"))
   320  	}
   321  	return nil
   322  }
   323  
   324  // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
   325  func areSubcommandsSupported(cmd *cobra.Command, details versionDetails) error {
   326  	clientVersion := details.Client().ClientVersion()
   327  	osType := details.ServerInfo().OSType
   328  	hasExperimental := details.ServerInfo().HasExperimental
   329  	hasExperimentalCLI := details.ClientInfo().HasExperimental
   330  
   331  	// Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack`
   332  	for curr := cmd; curr != nil; curr = curr.Parent() {
   333  		if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(clientVersion, cmdVersion) {
   334  			return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, clientVersion)
   335  		}
   336  		if os, ok := curr.Annotations["ostype"]; ok && os != osType {
   337  			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)
   338  		}
   339  		if _, ok := curr.Annotations["experimental"]; ok && !hasExperimental {
   340  			return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath())
   341  		}
   342  		if _, ok := curr.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI {
   343  			return fmt.Errorf("%s is only supported on a Docker cli with experimental cli features enabled", cmd.CommandPath())
   344  		}
   345  	}
   346  	return nil
   347  }
   348  
   349  func getFlagAnnotation(f *pflag.Flag, annotation string) string {
   350  	if value, ok := f.Annotations[annotation]; ok && len(value) == 1 {
   351  		return value[0]
   352  	}
   353  	return ""
   354  }
   355  
   356  func isVersionSupported(f *pflag.Flag, clientVersion string) bool {
   357  	if v := getFlagAnnotation(f, "version"); v != "" {
   358  		return versions.GreaterThanOrEqualTo(clientVersion, v)
   359  	}
   360  	return true
   361  }
   362  
   363  func isOSTypeSupported(f *pflag.Flag, osType string) bool {
   364  	if v := getFlagAnnotation(f, "ostype"); v != "" && osType != "" {
   365  		return osType == v
   366  	}
   367  	return true
   368  }
   369  
   370  // hasTags return true if any of the command's parents has tags
   371  func hasTags(cmd *cobra.Command) bool {
   372  	for curr := cmd; curr != nil; curr = curr.Parent() {
   373  		if len(curr.Annotations) > 0 {
   374  			return true
   375  		}
   376  	}
   377  
   378  	return false
   379  }