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