| // FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: | |
| //go:build go1.26 | |
| 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 { | |
| if stErr, ok := errors.AsType[cli.StatusError](err); ok { | |
| // 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 | |
| } |