mirror of
https://github.com/docker/cli.git
synced 2025-08-29 00:47:54 +03:00
allow plugins to have argument which match a top-level flag.
The issue with plugin options clashing with globals is that when cobra is parsing the command line and it comes across an argument which doesn't start with a `-` it (in the absence of plugins) distinguishes between "argument to current command" and "new subcommand" based on the list of registered sub commands. Plugins breaks that model. When presented with `docker -D plugin -c foo` cobra parses up to the `plugin`, sees it isn't a registered sub-command of the top-level docker (because it isn't, it's a plugin) so it accumulates it as an argument to the top-level `docker` command. Then it sees the `-c`, and thinks it is the global `-c` (for AKA `--context`) option and tries to treat it as that, which fails. In the specific case of the top-level `docker` subcommand we know that it has no arguments which aren't `--flags` (or `-f` short flags) and so anything which doesn't start with a `-` must either be a (known) subcommand or an attempt to execute a plugin. We could simply scan for and register all installed plugins at start of day, so that cobra can do the right thing, but we want to avoid that since it would involve executing each plugin to fetch the metadata, even if the command wasn't going to end up hitting a plugin. Instead we can parse the initial set of global arguments separately before hitting the main cobra `Execute` path, which works here exactly because we know that the top-level has no non-flag arguments. One slight wrinkle is that the top-level `PersistentPreRunE` is no longer called on the plugins path (since it no longer goes via `Execute`), so we arrange for the initialisation done there (which has to be done after global flags are parsed to handle e.g. `--config`) to happen explictly after the global flags are parsed. Rather than make `newDockerCommand` return the complicated set of results needed to make this happen, instead return a closure which achieves this. The new functionality is introduced via a common `TopLevelCommand` abstraction which lets us adjust the plugin entrypoint to use the same strategy for parsing the global arguments. This isn't strictly required (in this case the stuff in cobra's `Execute` works fine) but doing it this way avoids the possibility of subtle differences in behaviour. Fixes #1699, and also, as a side-effect, the first item in #1661. Signed-off-by: Ian Campbell <ijc@docker.com>
This commit is contained in:
@@ -4,18 +4,32 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/connhelper"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func runPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
|
||||
tcmd := newPluginCommand(dockerCli, plugin, meta)
|
||||
|
||||
// Doing this here avoids also calling it for the metadata
|
||||
// command which needlessly initializes the client and tries
|
||||
// to connect to the daemon.
|
||||
plugin.PersistentPreRunE = func(_ *cobra.Command, _ []string) error {
|
||||
return tcmd.Initialize(withPluginClientConn(plugin.Name()))
|
||||
}
|
||||
|
||||
cmd, _, err := tcmd.HandleGlobalFlags()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.Execute()
|
||||
}
|
||||
|
||||
// Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function.
|
||||
func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
|
||||
dockerCli, err := command.NewDockerCli()
|
||||
@@ -26,9 +40,7 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
|
||||
|
||||
plugin := makeCmd(dockerCli)
|
||||
|
||||
cmd := newPluginCommand(dockerCli, plugin, meta)
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
if err := runPlugin(dockerCli, plugin, meta); err != nil {
|
||||
if sterr, ok := err.(cli.StatusError); ok {
|
||||
if sterr.Status != "" {
|
||||
fmt.Fprintln(dockerCli.Err(), sterr.Status)
|
||||
@@ -45,40 +57,6 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
|
||||
}
|
||||
}
|
||||
|
||||
// options encapsulates the ClientOptions and FlagSet constructed by
|
||||
// `newPluginCommand` such that they can be finalized by our
|
||||
// `PersistentPreRunE`. This is necessary because otherwise a plugin's
|
||||
// own use of that hook will shadow anything we add to the top-level
|
||||
// command meaning the CLI is never Initialized.
|
||||
var options struct {
|
||||
name string
|
||||
init, prerun sync.Once
|
||||
opts *cliflags.ClientOptions
|
||||
flags *pflag.FlagSet
|
||||
dockerCli *command.DockerCli
|
||||
}
|
||||
|
||||
// PersistentPreRunE must be called by any plugin command (or
|
||||
// subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins
|
||||
// which do not make use of `PersistentPreRun*` do not need to call
|
||||
// this (although it remains safe to do so). Plugins are recommended
|
||||
// to use `PersistenPreRunE` to enable the error to be
|
||||
// returned. Should not be called outside of a commands
|
||||
// PersistentPreRunE hook and must not be run unless Run has been
|
||||
// called.
|
||||
func PersistentPreRunE(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
options.prerun.Do(func() {
|
||||
if options.opts == nil || options.flags == nil || options.dockerCli == nil {
|
||||
panic("PersistentPreRunE called without Run successfully called first")
|
||||
}
|
||||
// flags must be the original top-level command flags, not cmd.Flags()
|
||||
options.opts.Common.SetDefaultOptions(options.flags)
|
||||
err = options.dockerCli.Initialize(options.opts, withPluginClientConn(options.name))
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func withPluginClientConn(name string) command.InitializeOpt {
|
||||
return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
|
||||
cmd := "docker"
|
||||
@@ -111,7 +89,7 @@ func withPluginClientConn(name string) command.InitializeOpt {
|
||||
})
|
||||
}
|
||||
|
||||
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
|
||||
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand {
|
||||
name := plugin.Name()
|
||||
fullname := manager.NamePrefix + name
|
||||
|
||||
@@ -121,7 +99,6 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
TraverseChildren: true,
|
||||
PersistentPreRunE: PersistentPreRunE,
|
||||
DisableFlagsInUseLine: true,
|
||||
}
|
||||
opts, flags := cli.SetupPluginRootCommand(cmd)
|
||||
@@ -135,13 +112,7 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
||||
|
||||
cli.DisableFlagsInUseLine(cmd)
|
||||
|
||||
options.init.Do(func() {
|
||||
options.name = name
|
||||
options.opts = opts
|
||||
options.flags = flags
|
||||
options.dockerCli = dockerCli
|
||||
})
|
||||
return cmd
|
||||
return cli.NewTopLevelCommand(cmd, dockerCli, opts, flags)
|
||||
}
|
||||
|
||||
func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
|
||||
@@ -151,8 +122,6 @@ func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.
|
||||
cmd := &cobra.Command{
|
||||
Use: manager.MetadataSubcommandName,
|
||||
Hidden: true,
|
||||
// Suppress the global/parent PersistentPreRunE, which needlessly initializes the client and tries to connect to the daemon.
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetEscapeHTML(false)
|
||||
|
Reference in New Issue
Block a user