2018-12-10 10:30:19 -05:00
|
|
|
package plugin
|
|
|
|
|
|
|
|
import (
|
2023-10-12 15:20:16 -04:00
|
|
|
"context"
|
2018-12-10 10:30:19 -05:00
|
|
|
"encoding/json"
|
2023-10-12 15:20:16 -04:00
|
|
|
"errors"
|
2018-12-10 10:30:19 -05:00
|
|
|
"fmt"
|
2023-10-12 15:20:16 -04:00
|
|
|
"io"
|
|
|
|
"net"
|
2018-12-10 10:30:19 -05:00
|
|
|
"os"
|
2019-03-14 10:02:49 -04:00
|
|
|
"sync"
|
2018-12-10 10:30:19 -05:00
|
|
|
|
|
|
|
"github.com/docker/cli/cli"
|
|
|
|
"github.com/docker/cli/cli-plugins/manager"
|
|
|
|
"github.com/docker/cli/cli/command"
|
2019-01-31 12:50:58 -05:00
|
|
|
"github.com/docker/cli/cli/connhelper"
|
|
|
|
"github.com/docker/docker/client"
|
2018-12-10 10:30:19 -05:00
|
|
|
"github.com/spf13/cobra"
|
|
|
|
)
|
|
|
|
|
2023-10-12 15:20:16 -04:00
|
|
|
// CLIPluginSocketEnvKey is used to pass the plugin being
|
|
|
|
// executed the abstract socket name it should listen on to know
|
|
|
|
// when the CLI has exited.
|
|
|
|
const CLIPluginSocketEnvKey = "DOCKER_CLI_PLUGIN_SOCKET"
|
|
|
|
|
2019-03-14 10:02:49 -04:00
|
|
|
// 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 command's
|
|
|
|
// PersistentPreRunE hook and must not be run unless Run has been
|
|
|
|
// called.
|
|
|
|
var PersistentPreRunE func(*cobra.Command, []string) error
|
|
|
|
|
2023-10-12 15:20:16 -04:00
|
|
|
// closeOnCLISocketClose connects to the socket specified
|
|
|
|
// by the DOCKER_CLI_PLUGIN_SOCKET env var, if present, and attempts
|
|
|
|
// to read from it until it receives an EOF, which signals that
|
|
|
|
// the CLI is going to exit and the plugin should also exit.
|
|
|
|
func closeOnCLISocketClose(cancel func()) {
|
|
|
|
socketAddr, ok := os.LookupEnv(CLIPluginSocketEnvKey)
|
|
|
|
if !ok {
|
|
|
|
// if a plugin compiled against a more recent version of docker/cli
|
|
|
|
// is executed by an older CLI binary, ignore missing environment
|
|
|
|
// variable and behave as usual
|
|
|
|
return
|
|
|
|
}
|
|
|
|
addr, err := net.ResolveUnixAddr("unix", socketAddr)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
cliCloseConn, err := net.DialUnix("unix", nil, addr)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
b := make([]byte, 1)
|
|
|
|
for {
|
|
|
|
_, err := cliCloseConn.Read(b)
|
|
|
|
if errors.Is(err, io.EOF) {
|
|
|
|
cancel()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
2020-04-28 18:38:04 -04:00
|
|
|
// RunPlugin executes the specified plugin command
|
|
|
|
func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error {
|
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>
2019-03-06 05:29:42 -05:00
|
|
|
tcmd := newPluginCommand(dockerCli, plugin, meta)
|
|
|
|
|
2019-03-14 10:02:49 -04:00
|
|
|
var persistentPreRunOnce sync.Once
|
2023-10-12 15:20:16 -04:00
|
|
|
PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
|
2019-03-14 10:02:49 -04:00
|
|
|
var err error
|
|
|
|
persistentPreRunOnce.Do(func() {
|
2023-10-12 15:20:16 -04:00
|
|
|
cmdContext := cmd.Context()
|
|
|
|
// TODO: revisit and make sure this check makes sense
|
|
|
|
// see: https://github.com/docker/cli/pull/4599#discussion_r1422487271
|
|
|
|
if cmdContext == nil {
|
|
|
|
cmdContext = context.TODO()
|
|
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(cmdContext)
|
|
|
|
cmd.SetContext(ctx)
|
|
|
|
closeOnCLISocketClose(cancel)
|
|
|
|
|
2023-06-20 18:36:25 -04:00
|
|
|
var opts []command.CLIOption
|
2019-03-18 05:33:25 -04:00
|
|
|
if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" {
|
|
|
|
opts = append(opts, withPluginClientConn(plugin.Name()))
|
|
|
|
}
|
|
|
|
err = tcmd.Initialize(opts...)
|
2019-03-14 10:02:49 -04:00
|
|
|
})
|
|
|
|
return err
|
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>
2019-03-06 05:29:42 -05:00
|
|
|
}
|
|
|
|
|
2019-04-03 07:00:38 -04:00
|
|
|
cmd, args, err := tcmd.HandleGlobalFlags()
|
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>
2019-03-06 05:29:42 -05:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-04-03 07:00:38 -04:00
|
|
|
// We've parsed global args already, so reset args to those
|
|
|
|
// which remain.
|
|
|
|
cmd.SetArgs(args)
|
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>
2019-03-06 05:29:42 -05:00
|
|
|
return cmd.Execute()
|
|
|
|
}
|
|
|
|
|
2018-12-10 10:30:19 -05:00
|
|
|
// 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()
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
plugin := makeCmd(dockerCli)
|
|
|
|
|
2020-04-28 18:38:04 -04:00
|
|
|
if err := RunPlugin(dockerCli, plugin, meta); err != nil {
|
2018-12-10 10:30:19 -05:00
|
|
|
if sterr, ok := err.(cli.StatusError); ok {
|
|
|
|
if sterr.Status != "" {
|
|
|
|
fmt.Fprintln(dockerCli.Err(), sterr.Status)
|
|
|
|
}
|
|
|
|
// StatusError should only be used for errors, and all errors should
|
|
|
|
// have a non-zero exit status, so never exit with 0
|
|
|
|
if sterr.StatusCode == 0 {
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
os.Exit(sterr.StatusCode)
|
|
|
|
}
|
|
|
|
fmt.Fprintln(dockerCli.Err(), err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-20 18:36:25 -04:00
|
|
|
func withPluginClientConn(name string) command.CLIOption {
|
2019-01-31 12:50:58 -05:00
|
|
|
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))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
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>
2019-03-06 05:29:42 -05:00
|
|
|
func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand {
|
2019-02-01 05:34:44 -05:00
|
|
|
name := plugin.Name()
|
2018-12-10 10:30:19 -05:00
|
|
|
fullname := manager.NamePrefix + name
|
|
|
|
|
|
|
|
cmd := &cobra.Command{
|
2019-04-30 05:19:58 -04:00
|
|
|
Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name),
|
|
|
|
Short: fullname + " is a Docker CLI plugin",
|
|
|
|
SilenceUsage: true,
|
|
|
|
SilenceErrors: true,
|
|
|
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
// We can't use this as the hook directly since it is initialised later (in runPlugin)
|
|
|
|
return PersistentPreRunE(cmd, args)
|
|
|
|
},
|
2018-12-17 10:55:38 -05:00
|
|
|
TraverseChildren: true,
|
2018-12-10 10:30:19 -05:00
|
|
|
DisableFlagsInUseLine: true,
|
2022-03-30 09:27:25 -04:00
|
|
|
CompletionOptions: cobra.CompletionOptions{
|
|
|
|
DisableDefaultCmd: false,
|
|
|
|
HiddenDefaultCmd: true,
|
|
|
|
DisableDescriptions: true,
|
|
|
|
},
|
2018-12-10 10:30:19 -05:00
|
|
|
}
|
2023-06-28 10:06:19 -04:00
|
|
|
opts, _ := cli.SetupPluginRootCommand(cmd)
|
2018-12-10 10:30:19 -05:00
|
|
|
|
2022-04-01 06:24:44 -04:00
|
|
|
cmd.SetIn(dockerCli.In())
|
2020-05-07 08:25:59 -04:00
|
|
|
cmd.SetOut(dockerCli.Out())
|
2022-04-01 06:24:44 -04:00
|
|
|
cmd.SetErr(dockerCli.Err())
|
2018-12-10 10:30:19 -05:00
|
|
|
|
|
|
|
cmd.AddCommand(
|
|
|
|
plugin,
|
|
|
|
newMetadataSubcommand(plugin, meta),
|
|
|
|
)
|
|
|
|
|
|
|
|
cli.DisableFlagsInUseLine(cmd)
|
|
|
|
|
2023-06-28 10:06:19 -04:00
|
|
|
return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags())
|
2018-12-10 10:30:19 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command {
|
|
|
|
if meta.ShortDescription == "" {
|
|
|
|
meta.ShortDescription = plugin.Short
|
|
|
|
}
|
|
|
|
cmd := &cobra.Command{
|
|
|
|
Use: manager.MetadataSubcommandName,
|
|
|
|
Hidden: true,
|
2019-03-14 10:02:49 -04:00
|
|
|
// Suppress the global/parent PersistentPreRunE, which
|
|
|
|
// needlessly initializes the client and tries to
|
|
|
|
// connect to the daemon.
|
|
|
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {},
|
2018-12-10 10:30:19 -05:00
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
enc := json.NewEncoder(os.Stdout)
|
|
|
|
enc.SetEscapeHTML(false)
|
|
|
|
enc.SetIndent("", " ")
|
|
|
|
return enc.Encode(meta)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
return cmd
|
|
|
|
}
|
2022-02-23 04:16:47 -05:00
|
|
|
|
|
|
|
// RunningStandalone tells a CLI plugin it is run standalone by direct execution
|
|
|
|
func RunningStandalone() bool {
|
|
|
|
if os.Getenv(manager.ReexecEnvvar) != "" {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName
|
|
|
|
}
|