github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/cmd/docker/docker.go (about)

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