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