github.1git.de/docker/cli@v26.1.3+incompatible/cli-plugins/plugin/plugin.go (about)

     1  package plugin
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"sync"
     9  
    10  	"github.com/docker/cli/cli"
    11  	"github.com/docker/cli/cli-plugins/manager"
    12  	"github.com/docker/cli/cli-plugins/socket"
    13  	"github.com/docker/cli/cli/command"
    14  	"github.com/docker/cli/cli/connhelper"
    15  	"github.com/docker/cli/cli/debug"
    16  	"github.com/docker/docker/client"
    17  	"github.com/spf13/cobra"
    18  	"go.opentelemetry.io/otel"
    19  )
    20  
    21  // PersistentPreRunE must be called by any plugin command (or
    22  // subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
    23  // which do not make use of `PersistentPreRun*` do not need to call
    24  // this (although it remains safe to do so). Plugins are recommended
    25  // to use `PersistentPreRunE` to enable the error to be
    26  // returned. Should not be called outside of a command's
    27  // PersistentPreRunE hook and must not be run unless Run has been
    28  // called.
    29  var PersistentPreRunE func(*cobra.Command, []string) error
    30  
    31  // RunPlugin executes the specified plugin command
    32  func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
    33  	tcmd := newPluginCommand(dockerCli, plugin, meta)
    34  
    35  	var persistentPreRunOnce sync.Once
    36  	PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
    37  		var err error
    38  		persistentPreRunOnce.Do(func() {
    39  			cmdContext := cmd.Context()
    40  			// TODO: revisit and make sure this check makes sense
    41  			// see: https://github.com/docker/cli/pull/4599#discussion_r1422487271
    42  			if cmdContext == nil {
    43  				cmdContext = context.TODO()
    44  			}
    45  			ctx, cancel := context.WithCancel(cmdContext)
    46  			cmd.SetContext(ctx)
    47  			// Set up the context to cancel based on signalling via CLI socket.
    48  			socket.ConnectAndWait(cancel)
    49  
    50  			var opts []command.CLIOption
    51  			if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" {
    52  				opts = append(opts, withPluginClientConn(plugin.Name()))
    53  			}
    54  			err = tcmd.Initialize(opts...)
    55  			ogRunE := cmd.RunE
    56  			if ogRunE == nil {
    57  				ogRun := cmd.Run
    58  				// necessary because error will always be nil here
    59  				// see: https://github.com/golangci/golangci-lint/issues/1379
    60  				//nolint:unparam
    61  				ogRunE = func(cmd *cobra.Command, args []string) error {
    62  					ogRun(cmd, args)
    63  					return nil
    64  				}
    65  				cmd.Run = nil
    66  			}
    67  			cmd.RunE = func(cmd *cobra.Command, args []string) error {
    68  				stopInstrumentation := dockerCli.StartInstrumentation(cmd)
    69  				err := ogRunE(cmd, args)
    70  				stopInstrumentation(err)
    71  				return err
    72  			}
    73  		})
    74  		return err
    75  	}
    76  
    77  	cmd, args, err := tcmd.HandleGlobalFlags()
    78  	if err != nil {
    79  		return err
    80  	}
    81  	// We've parsed global args already, so reset args to those
    82  	// which remain.
    83  	cmd.SetArgs(args)
    84  	return cmd.Execute()
    85  }
    86  
    87  // Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
    88  func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
    89  	otel.SetErrorHandler(debug.OTELErrorHandler)
    90  
    91  	dockerCli, err := command.NewDockerCli()
    92  	if err != nil {
    93  		fmt.Fprintln(os.Stderr, err)
    94  		os.Exit(1)
    95  	}
    96  
    97  	plugin := makeCmd(dockerCli)
    98  
    99  	if err := RunPlugin(dockerCli, plugin, meta); err != nil {
   100  		if sterr, ok := err.(cli.StatusError); ok {
   101  			if sterr.Status != "" {
   102  				fmt.Fprintln(dockerCli.Err(), sterr.Status)
   103  			}
   104  			// StatusError should only be used for errors, and all errors should
   105  			// have a non-zero exit status, so never exit with 0
   106  			if sterr.StatusCode == 0 {
   107  				os.Exit(1)
   108  			}
   109  			os.Exit(sterr.StatusCode)
   110  		}
   111  		fmt.Fprintln(dockerCli.Err(), err)
   112  		os.Exit(1)
   113  	}
   114  }
   115  
   116  func withPluginClientConn(name string) command.CLIOption {
   117  	return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
   118  		cmd := "docker"
   119  		if x := os.Getenv(manager.ReexecEnvvar); x != "" {
   120  			cmd = x
   121  		}
   122  		var flags []string
   123  
   124  		// Accumulate all the global arguments, that is those
   125  		// up to (but not including) the plugin's name. This
   126  		// ensures that `docker system dial-stdio` is
   127  		// evaluating the same set of `--config`, `--tls*` etc
   128  		// global options as the plugin was called with, which
   129  		// in turn is the same as what the original docker
   130  		// invocation was passed.
   131  		for _, a := range os.Args[1:] {
   132  			if a == name {
   133  				break
   134  			}
   135  			flags = append(flags, a)
   136  		}
   137  		flags = append(flags, "system", "dial-stdio")
   138  
   139  		helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...)
   140  		if err != nil {
   141  			return nil, err
   142  		}
   143  
   144  		return client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
   145  	})
   146  }
   147  
   148  func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand {
   149  	name := plugin.Name()
   150  	fullname := manager.NamePrefix + name
   151  
   152  	cmd := &cobra.Command{
   153  		Use:           fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
   154  		Short:         fullname + " is a Docker CLI plugin",
   155  		SilenceUsage:  true,
   156  		SilenceErrors: true,
   157  		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
   158  			// We can't use this as the hook directly since it is initialised later (in runPlugin)
   159  			return PersistentPreRunE(cmd, args)
   160  		},
   161  		TraverseChildren:      true,
   162  		DisableFlagsInUseLine: true,
   163  		CompletionOptions: cobra.CompletionOptions{
   164  			DisableDefaultCmd:   false,
   165  			HiddenDefaultCmd:    true,
   166  			DisableDescriptions: true,
   167  		},
   168  	}
   169  	opts, _ := cli.SetupPluginRootCommand(cmd)
   170  
   171  	cmd.SetIn(dockerCli.In())
   172  	cmd.SetOut(dockerCli.Out())
   173  	cmd.SetErr(dockerCli.Err())
   174  
   175  	cmd.AddCommand(
   176  		plugin,
   177  		newMetadataSubcommand(plugin, meta),
   178  	)
   179  
   180  	cli.DisableFlagsInUseLine(cmd)
   181  
   182  	return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
   183  }
   184  
   185  func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
   186  	if meta.ShortDescription == "" {
   187  		meta.ShortDescription = plugin.Short
   188  	}
   189  	cmd := &cobra.Command{
   190  		Use:    manager.MetadataSubcommandName,
   191  		Hidden: true,
   192  		// Suppress the global/parent PersistentPreRunE, which
   193  		// needlessly initializes the client and tries to
   194  		// connect to the daemon.
   195  		PersistentPreRun: func(cmd *cobra.Command, args []string) {},
   196  		RunE: func(cmd *cobra.Command, args []string) error {
   197  			enc := json.NewEncoder(os.Stdout)
   198  			enc.SetEscapeHTML(false)
   199  			enc.SetIndent("", "     ")
   200  			return enc.Encode(meta)
   201  		},
   202  	}
   203  	return cmd
   204  }
   205  
   206  // RunningStandalone tells a CLI plugin it is run standalone by direct execution
   207  func RunningStandalone() bool {
   208  	if os.Getenv(manager.ReexecEnvvar) != "" {
   209  		return false
   210  	}
   211  	return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName
   212  }