mirror of https://github.com/docker/cli.git
Merge pull request #1654 from ijc/plugins-dial-stdio
cli-plugins: use system dial-stdio to contact the engine.
This commit is contained in:
commit
06b837a7d7
|
@ -12,6 +12,12 @@ import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ReexecEnvvar is the name of an ennvar which is set to the command
|
||||||
|
// used to originally invoke the docker CLI when executing a
|
||||||
|
// plugin. Assuming $PATH and $CWD remain unchanged this should allow
|
||||||
|
// the plugin to re-execute the original CLI.
|
||||||
|
const ReexecEnvvar = "DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND"
|
||||||
|
|
||||||
// errPluginNotFound is the error returned when a plugin could not be found.
|
// errPluginNotFound is the error returned when a plugin could not be found.
|
||||||
type errPluginNotFound string
|
type errPluginNotFound string
|
||||||
|
|
||||||
|
@ -155,6 +161,9 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
|
|
||||||
|
cmd.Env = os.Environ()
|
||||||
|
cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0])
|
||||||
|
|
||||||
return cmd, nil
|
return cmd, nil
|
||||||
}
|
}
|
||||||
return nil, errPluginNotFound(name)
|
return nil, errPluginNotFound(name)
|
||||||
|
|
|
@ -9,7 +9,9 @@ import (
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli-plugins/manager"
|
"github.com/docker/cli/cli-plugins/manager"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/docker/cli/cli/connhelper"
|
||||||
cliflags "github.com/docker/cli/cli/flags"
|
cliflags "github.com/docker/cli/cli/flags"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
@ -49,6 +51,7 @@ func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) {
|
||||||
// own use of that hook will shadow anything we add to the top-level
|
// own use of that hook will shadow anything we add to the top-level
|
||||||
// command meaning the CLI is never Initialized.
|
// command meaning the CLI is never Initialized.
|
||||||
var options struct {
|
var options struct {
|
||||||
|
name string
|
||||||
init, prerun sync.Once
|
init, prerun sync.Once
|
||||||
opts *cliflags.ClientOptions
|
opts *cliflags.ClientOptions
|
||||||
flags *pflag.FlagSet
|
flags *pflag.FlagSet
|
||||||
|
@ -71,13 +74,45 @@ func PersistentPreRunE(cmd *cobra.Command, args []string) error {
|
||||||
}
|
}
|
||||||
// flags must be the original top-level command flags, not cmd.Flags()
|
// flags must be the original top-level command flags, not cmd.Flags()
|
||||||
options.opts.Common.SetDefaultOptions(options.flags)
|
options.opts.Common.SetDefaultOptions(options.flags)
|
||||||
err = options.dockerCli.Initialize(options.opts)
|
err = options.dockerCli.Initialize(options.opts, withPluginClientConn(options.name))
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func withPluginClientConn(name string) command.InitializeOpt {
|
||||||
|
return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) {
|
||||||
|
cmd := "docker"
|
||||||
|
if x := os.Getenv(manager.ReexecEnvvar); x != "" {
|
||||||
|
cmd = x
|
||||||
|
}
|
||||||
|
var flags []string
|
||||||
|
|
||||||
|
// Accumulate all the global arguments, that is those
|
||||||
|
// up to (but not including) the plugin's name. This
|
||||||
|
// ensures that `docker system dial-stdio` is
|
||||||
|
// evaluating the same set of `--config`, `--tls*` etc
|
||||||
|
// global options as the plugin was called with, which
|
||||||
|
// in turn is the same as what the original docker
|
||||||
|
// invocation was passed.
|
||||||
|
for _, a := range os.Args[1:] {
|
||||||
|
if a == name {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
flags = append(flags, a)
|
||||||
|
}
|
||||||
|
flags = append(flags, "system", "dial-stdio")
|
||||||
|
|
||||||
|
helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.NewClientWithOpts(client.WithDialContext(helper.Dialer))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
|
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
|
||||||
name := plugin.Use
|
name := plugin.Name()
|
||||||
fullname := manager.NamePrefix + name
|
fullname := manager.NamePrefix + name
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
|
@ -101,6 +136,7 @@ func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta
|
||||||
cli.DisableFlagsInUseLine(cmd)
|
cli.DisableFlagsInUseLine(cmd)
|
||||||
|
|
||||||
options.init.Do(func() {
|
options.init.Do(func() {
|
||||||
|
options.name = name
|
||||||
options.opts = opts
|
options.opts = opts
|
||||||
options.flags = flags
|
options.flags = flags
|
||||||
options.dockerCli = dockerCli
|
options.dockerCli = dockerCli
|
||||||
|
@ -115,6 +151,8 @@ func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: manager.MetadataSubcommandName,
|
Use: manager.MetadataSubcommandName,
|
||||||
Hidden: true,
|
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 {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
|
|
|
@ -175,9 +175,28 @@ func (cli *DockerCli) RegistryClient(allowInsecure bool) registryclient.Registry
|
||||||
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
|
return registryclient.NewRegistryClient(resolver, UserAgent(), allowInsecure)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitializeOpt is the type of the functional options passed to DockerCli.Initialize
|
||||||
|
type InitializeOpt func(dockerCli *DockerCli) error
|
||||||
|
|
||||||
|
// WithInitializeClient is passed to DockerCli.Initialize by callers who wish to set a particular API Client for use by the CLI.
|
||||||
|
func WithInitializeClient(makeClient func(dockerCli *DockerCli) (client.APIClient, error)) InitializeOpt {
|
||||||
|
return func(dockerCli *DockerCli) error {
|
||||||
|
var err error
|
||||||
|
dockerCli.client, err = makeClient(dockerCli)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the dockerCli runs initialization that must happen after command
|
// Initialize the dockerCli runs initialization that must happen after command
|
||||||
// line flags are parsed.
|
// line flags are parsed.
|
||||||
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...InitializeOpt) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for _, o := range ops {
|
||||||
|
if err := o(cli); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
cliflags.SetLogLevel(opts.Common.LogLevel)
|
cliflags.SetLogLevel(opts.Common.LogLevel)
|
||||||
|
|
||||||
if opts.ConfigDir != "" {
|
if opts.ConfigDir != "" {
|
||||||
|
@ -189,29 +208,31 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
|
cli.configFile = cliconfig.LoadDefaultConfigFile(cli.err)
|
||||||
var err error
|
|
||||||
cli.contextStore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
|
|
||||||
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
endpoint, err := resolveDockerEndpoint(cli.contextStore, cli.currentContext, opts.Common)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "unable to resolve docker endpoint")
|
|
||||||
}
|
|
||||||
cli.dockerEndpoint = endpoint
|
|
||||||
|
|
||||||
cli.client, err = newAPIClientFromEndpoint(endpoint, cli.configFile)
|
if cli.client == nil {
|
||||||
if tlsconfig.IsErrEncryptedKey(err) {
|
cli.contextStore = store.New(cliconfig.ContextStoreDir(), cli.contextStoreConfig)
|
||||||
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
|
cli.currentContext, err = resolveContextName(opts.Common, cli.configFile, cli.contextStore)
|
||||||
newClient := func(password string) (client.APIClient, error) {
|
if err != nil {
|
||||||
endpoint.TLSPassword = password
|
return err
|
||||||
return newAPIClientFromEndpoint(endpoint, cli.configFile)
|
}
|
||||||
|
endpoint, err := resolveDockerEndpoint(cli.contextStore, cli.currentContext, opts.Common)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "unable to resolve docker endpoint")
|
||||||
|
}
|
||||||
|
cli.dockerEndpoint = endpoint
|
||||||
|
|
||||||
|
cli.client, err = newAPIClientFromEndpoint(endpoint, cli.configFile)
|
||||||
|
if tlsconfig.IsErrEncryptedKey(err) {
|
||||||
|
passRetriever := passphrase.PromptRetrieverWithInOut(cli.In(), cli.Out(), nil)
|
||||||
|
newClient := func(password string) (client.APIClient, error) {
|
||||||
|
endpoint.TLSPassword = password
|
||||||
|
return newAPIClientFromEndpoint(endpoint, cli.configFile)
|
||||||
|
}
|
||||||
|
cli.client, err = getClientWithPassword(passRetriever, newClient)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
cli.client, err = getClientWithPassword(passRetriever, newClient)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
var experimentalValue string
|
var experimentalValue string
|
||||||
// Environment variable always overrides configuration
|
// Environment variable always overrides configuration
|
||||||
|
|
|
@ -175,6 +175,9 @@ func TestExperimentalCLI(t *testing.T) {
|
||||||
defer dir.Remove()
|
defer dir.Remove()
|
||||||
apiclient := &fakeClient{
|
apiclient := &fakeClient{
|
||||||
version: defaultVersion,
|
version: defaultVersion,
|
||||||
|
pingFunc: func() (types.Ping, error) {
|
||||||
|
return types.Ping{Experimental: true, OSType: "linux", APIVersion: defaultVersion}, nil
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cli := &DockerCli{client: apiclient, err: os.Stderr}
|
cli := &DockerCli{client: apiclient, err: os.Stderr}
|
||||||
|
|
|
@ -53,6 +53,16 @@ func GetConnectionHelper(daemonURL string) (*ConnectionHelper, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCommandConnectionHelper returns a ConnectionHelp constructed from an arbitrary command.
|
||||||
|
func GetCommandConnectionHelper(cmd string, flags ...string) (*ConnectionHelper, error) {
|
||||||
|
return &ConnectionHelper{
|
||||||
|
Dialer: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return newCommandConn(ctx, cmd, flags...)
|
||||||
|
},
|
||||||
|
Host: "http://docker",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func newCommandConn(ctx context.Context, cmd string, args ...string) (net.Conn, error) {
|
func newCommandConn(ctx context.Context, cmd string, args ...string) (net.Conn, error) {
|
||||||
var (
|
var (
|
||||||
c commandConn
|
c commandConn
|
||||||
|
|
|
@ -75,6 +75,19 @@ A plugin is required to support all of the global options of the
|
||||||
top-level CLI, i.e. those listed by `man docker 1` with the exception
|
top-level CLI, i.e. those listed by `man docker 1` with the exception
|
||||||
of `-v`.
|
of `-v`.
|
||||||
|
|
||||||
|
## Connecting to the docker engine
|
||||||
|
|
||||||
|
For consistency plugins should prefer to dial the engine by using the
|
||||||
|
`system dial-stdio` subcommand of the main Docker CLI binary.
|
||||||
|
|
||||||
|
To facilitate this plugins will be executed with the
|
||||||
|
`$DOCKER_CLI_PLUGIN_ORIGINAL_CLI_COMMAND` environment variable
|
||||||
|
pointing back to the main Docker CLI binary.
|
||||||
|
|
||||||
|
All global options (everything from after the binary name up to, but
|
||||||
|
not including, the primary entry point subcommand name) should be
|
||||||
|
passed back to the CLI.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Plugins distributed in packages for system wide installation on
|
Plugins distributed in packages for system wide installation on
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package cliplugins
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli-plugins/manager"
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
"gotest.tools/icmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDialStdio(t *testing.T) {
|
||||||
|
// Run the helloworld plugin forcing /bin/true as the `system
|
||||||
|
// dial-stdio` target. It should be passed all arguments from
|
||||||
|
// before the `helloworld` arg, but not the --who=foo which
|
||||||
|
// follows. We observe this from the debug level logging from
|
||||||
|
// the connhelper stuff.
|
||||||
|
helloworld := filepath.Join(os.Getenv("DOCKER_CLI_E2E_PLUGINS_EXTRA_DIRS"), "docker-helloworld")
|
||||||
|
cmd := icmd.Command(helloworld, "--config=blah", "--tls", "--log-level", "debug", "helloworld", "--who=foo")
|
||||||
|
res := icmd.RunCmd(cmd, icmd.WithEnv(manager.ReexecEnvvar+"=/bin/true"))
|
||||||
|
res.Assert(t, icmd.Success)
|
||||||
|
assert.Assert(t, is.Contains(res.Stderr(), `msg="connhelper: starting /bin/true with [--config=blah --tls --log-level debug system dial-stdio]"`))
|
||||||
|
assert.Assert(t, is.Equal(res.Stdout(), "Hello foo!\n"))
|
||||||
|
}
|
Loading…
Reference in New Issue