github.com/pwn-term/docker@v0.0.0-20210616085119-6e977cce2565/cli/cli-plugins/plugin/plugin.go (about)

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