...

Source file src/github.com/docker/cli/cli-plugins/plugin/plugin.go

Documentation: github.com/docker/cli/cli-plugins/plugin

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

View as plain text