| package plugin |
| |
| import ( |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "os" |
| "sync" |
| |
| "github.com/docker/cli/cli" |
| "github.com/docker/cli/cli-plugins/metadata" |
| "github.com/docker/cli/cli-plugins/socket" |
| "github.com/docker/cli/cli/command" |
| "github.com/docker/cli/cli/connhelper" |
| "github.com/docker/cli/cli/debug" |
| "github.com/moby/moby/client" |
| "github.com/spf13/cobra" |
| "go.opentelemetry.io/otel" |
| ) |
| |
| // 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 `PersistentPreRunE` 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 |
| |
| // RunPlugin executes the specified plugin command |
| func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) error { |
| tcmd := newPluginCommand(dockerCli, plugin, meta) |
| |
| var persistentPreRunOnce sync.Once |
| PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { |
| var retErr error |
| persistentPreRunOnce.Do(func() { |
| ctx, cancel := context.WithCancel(cmd.Context()) |
| cmd.SetContext(ctx) |
| // Set up the context to cancel based on signalling via CLI socket. |
| socket.ConnectAndWait(cancel) |
| |
| var opts []command.CLIOption |
| if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" { |
| opts = append(opts, withPluginClientConn(plugin.Name())) |
| } |
| opts = append(opts, command.WithEnableGlobalMeterProvider(), command.WithEnableGlobalTracerProvider()) |
| retErr = tcmd.Initialize(opts...) |
| ogRunE := cmd.RunE |
| if ogRunE == nil { |
| ogRun := cmd.Run |
| // necessary because error will always be nil here |
| // see: https://github.com/golangci/golangci-lint/issues/1379 |
| //nolint:unparam |
| ogRunE = func(cmd *cobra.Command, args []string) error { |
| ogRun(cmd, args) |
| return nil |
| } |
| cmd.Run = nil |
| } |
| cmd.RunE = func(cmd *cobra.Command, args []string) error { |
| stopInstrumentation := dockerCli.StartInstrumentation(cmd) |
| err := ogRunE(cmd, args) |
| stopInstrumentation(err) |
| return err |
| } |
| }) |
| return retErr |
| } |
| |
| cmd, args, err := tcmd.HandleGlobalFlags() |
| if err != nil { |
| return err |
| } |
| // We've parsed global args already, so reset args to those |
| // which remain. |
| cmd.SetArgs(args) |
| return cmd.Execute() |
| } |
| |
| // Run is the top-level entry point to the CLI plugin framework. It should |
| // be called from the plugin's "main()" function. It initializes a new |
| // [command.DockerCli] instance with the given options before calling |
| // makeCmd to construct the plugin command, then invokes the plugin command |
| // using [RunPlugin]. |
| func Run(makeCmd func(command.Cli) *cobra.Command, meta metadata.Metadata, ops ...command.CLIOption) { |
| otel.SetErrorHandler(debug.OTELErrorHandler) |
| |
| dockerCLI, err := command.NewDockerCli(ops...) |
| if err != nil { |
| _, _ = fmt.Fprintln(os.Stderr, err) |
| os.Exit(1) |
| } |
| |
| plugin := makeCmd(dockerCLI) |
| |
| if err := RunPlugin(dockerCLI, plugin, meta); err != nil { |
| var stErr cli.StatusError |
| if errors.As(err, &stErr) { |
| // 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 { // FIXME(thaJeztah): this should never be used with a zero status-code. Check if we do this anywhere. |
| stErr.StatusCode = 1 |
| } |
| _, _ = fmt.Fprintln(dockerCLI.Err(), stErr) |
| os.Exit(stErr.StatusCode) |
| } |
| _, _ = fmt.Fprintln(dockerCLI.Err(), err) |
| os.Exit(1) |
| } |
| } |
| |
| func withPluginClientConn(name string) command.CLIOption { |
| return func(cli *command.DockerCli) error { |
| cmd := "docker" |
| if x := os.Getenv(metadata.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 err |
| } |
| apiClient, err := client.New(client.WithDialContext(helper.Dialer)) |
| if err != nil { |
| return err |
| } |
| return command.WithAPIClient(apiClient)(cli) |
| } |
| } |
| |
| func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta metadata.Metadata) *cli.TopLevelCommand { |
| name := plugin.Name() |
| fullname := metadata.NamePrefix + name |
| |
| cmd := &cobra.Command{ |
| 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) |
| }, |
| TraverseChildren: true, |
| DisableFlagsInUseLine: true, |
| CompletionOptions: cobra.CompletionOptions{ |
| DisableDefaultCmd: false, |
| HiddenDefaultCmd: true, |
| DisableDescriptions: os.Getenv("DOCKER_CLI_DISABLE_COMPLETION_DESCRIPTION") != "", |
| }, |
| } |
| |
| // Disable file-completion by default. Most commands and flags should not |
| // complete with filenames. |
| cmd.CompletionOptions.SetDefaultShellCompDirective(cobra.ShellCompDirectiveNoFileComp) |
| |
| opts, _ := cli.SetupPluginRootCommand(cmd) |
| |
| cmd.SetIn(dockerCli.In()) |
| cmd.SetOut(dockerCli.Out()) |
| cmd.SetErr(dockerCli.Err()) |
| |
| cmd.AddCommand( |
| plugin, |
| newMetadataSubcommand(plugin, meta), |
| ) |
| |
| visitAll(cmd, |
| // prevent adding "[flags]" to the end of the usage line. |
| func(c *cobra.Command) { c.DisableFlagsInUseLine = true }, |
| ) |
| |
| return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags()) |
| } |
| |
| // visitAll traverses all commands from the root. |
| func visitAll(root *cobra.Command, fns ...func(*cobra.Command)) { |
| for _, cmd := range root.Commands() { |
| visitAll(cmd, fns...) |
| } |
| for _, fn := range fns { |
| fn(root) |
| } |
| } |
| |
| func newMetadataSubcommand(plugin *cobra.Command, meta metadata.Metadata) *cobra.Command { |
| if meta.ShortDescription == "" { |
| meta.ShortDescription = plugin.Short |
| } |
| cmd := &cobra.Command{ |
| Use: metadata.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) |
| enc.SetIndent("", " ") |
| return enc.Encode(meta) |
| }, |
| } |
| return cmd |
| } |
| |
| // RunningStandalone tells a CLI plugin it is run standalone by direct execution |
| func RunningStandalone() bool { |
| if os.Getenv(metadata.ReexecEnvvar) != "" { |
| return false |
| } |
| return len(os.Args) < 2 || os.Args[1] != metadata.MetadataSubcommandName |
| } |