github.1git.de/docker/cli@v26.1.3+incompatible/cli-plugins/plugin/plugin.go (about) 1 package plugin 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "os" 8 "sync" 9 10 "github.com/docker/cli/cli" 11 "github.com/docker/cli/cli-plugins/manager" 12 "github.com/docker/cli/cli-plugins/socket" 13 "github.com/docker/cli/cli/command" 14 "github.com/docker/cli/cli/connhelper" 15 "github.com/docker/cli/cli/debug" 16 "github.com/docker/docker/client" 17 "github.com/spf13/cobra" 18 "go.opentelemetry.io/otel" 19 ) 20 21 // PersistentPreRunE must be called by any plugin command (or 22 // subcommand) which uses the cobra `PersistentPreRun*` hook. Plugins 23 // which do not make use of `PersistentPreRun*` do not need to call 24 // this (although it remains safe to do so). Plugins are recommended 25 // to use `PersistentPreRunE` to enable the error to be 26 // returned. Should not be called outside of a command's 27 // PersistentPreRunE hook and must not be run unless Run has been 28 // called. 29 var PersistentPreRunE func(*cobra.Command, []string) error 30 31 // RunPlugin executes the specified plugin command 32 func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) error { 33 tcmd := newPluginCommand(dockerCli, plugin, meta) 34 35 var persistentPreRunOnce sync.Once 36 PersistentPreRunE = func(cmd *cobra.Command, _ []string) error { 37 var err error 38 persistentPreRunOnce.Do(func() { 39 cmdContext := cmd.Context() 40 // TODO: revisit and make sure this check makes sense 41 // see: https://github.com/docker/cli/pull/4599#discussion_r1422487271 42 if cmdContext == nil { 43 cmdContext = context.TODO() 44 } 45 ctx, cancel := context.WithCancel(cmdContext) 46 cmd.SetContext(ctx) 47 // Set up the context to cancel based on signalling via CLI socket. 48 socket.ConnectAndWait(cancel) 49 50 var opts []command.CLIOption 51 if os.Getenv("DOCKER_CLI_PLUGIN_USE_DIAL_STDIO") != "" { 52 opts = append(opts, withPluginClientConn(plugin.Name())) 53 } 54 err = tcmd.Initialize(opts...) 55 ogRunE := cmd.RunE 56 if ogRunE == nil { 57 ogRun := cmd.Run 58 // necessary because error will always be nil here 59 // see: https://github.com/golangci/golangci-lint/issues/1379 60 //nolint:unparam 61 ogRunE = func(cmd *cobra.Command, args []string) error { 62 ogRun(cmd, args) 63 return nil 64 } 65 cmd.Run = nil 66 } 67 cmd.RunE = func(cmd *cobra.Command, args []string) error { 68 stopInstrumentation := dockerCli.StartInstrumentation(cmd) 69 err := ogRunE(cmd, args) 70 stopInstrumentation(err) 71 return err 72 } 73 }) 74 return err 75 } 76 77 cmd, args, err := tcmd.HandleGlobalFlags() 78 if err != nil { 79 return err 80 } 81 // We've parsed global args already, so reset args to those 82 // which remain. 83 cmd.SetArgs(args) 84 return cmd.Execute() 85 } 86 87 // Run is the top-level entry point to the CLI plugin framework. It should be called from your plugin's `main()` function. 88 func Run(makeCmd func(command.Cli) *cobra.Command, meta manager.Metadata) { 89 otel.SetErrorHandler(debug.OTELErrorHandler) 90 91 dockerCli, err := command.NewDockerCli() 92 if err != nil { 93 fmt.Fprintln(os.Stderr, err) 94 os.Exit(1) 95 } 96 97 plugin := makeCmd(dockerCli) 98 99 if err := RunPlugin(dockerCli, plugin, meta); err != nil { 100 if sterr, ok := err.(cli.StatusError); ok { 101 if sterr.Status != "" { 102 fmt.Fprintln(dockerCli.Err(), sterr.Status) 103 } 104 // StatusError should only be used for errors, and all errors should 105 // have a non-zero exit status, so never exit with 0 106 if sterr.StatusCode == 0 { 107 os.Exit(1) 108 } 109 os.Exit(sterr.StatusCode) 110 } 111 fmt.Fprintln(dockerCli.Err(), err) 112 os.Exit(1) 113 } 114 } 115 116 func withPluginClientConn(name string) command.CLIOption { 117 return command.WithInitializeClient(func(dockerCli *command.DockerCli) (client.APIClient, error) { 118 cmd := "docker" 119 if x := os.Getenv(manager.ReexecEnvvar); x != "" { 120 cmd = x 121 } 122 var flags []string 123 124 // Accumulate all the global arguments, that is those 125 // up to (but not including) the plugin's name. This 126 // ensures that `docker system dial-stdio` is 127 // evaluating the same set of `--config`, `--tls*` etc 128 // global options as the plugin was called with, which 129 // in turn is the same as what the original docker 130 // invocation was passed. 131 for _, a := range os.Args[1:] { 132 if a == name { 133 break 134 } 135 flags = append(flags, a) 136 } 137 flags = append(flags, "system", "dial-stdio") 138 139 helper, err := connhelper.GetCommandConnectionHelper(cmd, flags...) 140 if err != nil { 141 return nil, err 142 } 143 144 return client.NewClientWithOpts(client.WithDialContext(helper.Dialer)) 145 }) 146 } 147 148 func newPluginCommand(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager.Metadata) *cli.TopLevelCommand { 149 name := plugin.Name() 150 fullname := manager.NamePrefix + name 151 152 cmd := &cobra.Command{ 153 Use: fmt.Sprintf("docker [OPTIONS] %s [ARG...]", name), 154 Short: fullname + " is a Docker CLI plugin", 155 SilenceUsage: true, 156 SilenceErrors: true, 157 PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 158 // We can't use this as the hook directly since it is initialised later (in runPlugin) 159 return PersistentPreRunE(cmd, args) 160 }, 161 TraverseChildren: true, 162 DisableFlagsInUseLine: true, 163 CompletionOptions: cobra.CompletionOptions{ 164 DisableDefaultCmd: false, 165 HiddenDefaultCmd: true, 166 DisableDescriptions: true, 167 }, 168 } 169 opts, _ := cli.SetupPluginRootCommand(cmd) 170 171 cmd.SetIn(dockerCli.In()) 172 cmd.SetOut(dockerCli.Out()) 173 cmd.SetErr(dockerCli.Err()) 174 175 cmd.AddCommand( 176 plugin, 177 newMetadataSubcommand(plugin, meta), 178 ) 179 180 cli.DisableFlagsInUseLine(cmd) 181 182 return cli.NewTopLevelCommand(cmd, dockerCli, opts, cmd.Flags()) 183 } 184 185 func newMetadataSubcommand(plugin *cobra.Command, meta manager.Metadata) *cobra.Command { 186 if meta.ShortDescription == "" { 187 meta.ShortDescription = plugin.Short 188 } 189 cmd := &cobra.Command{ 190 Use: manager.MetadataSubcommandName, 191 Hidden: true, 192 // Suppress the global/parent PersistentPreRunE, which 193 // needlessly initializes the client and tries to 194 // connect to the daemon. 195 PersistentPreRun: func(cmd *cobra.Command, args []string) {}, 196 RunE: func(cmd *cobra.Command, args []string) error { 197 enc := json.NewEncoder(os.Stdout) 198 enc.SetEscapeHTML(false) 199 enc.SetIndent("", " ") 200 return enc.Encode(meta) 201 }, 202 } 203 return cmd 204 } 205 206 // RunningStandalone tells a CLI plugin it is run standalone by direct execution 207 func RunningStandalone() bool { 208 if os.Getenv(manager.ReexecEnvvar) != "" { 209 return false 210 } 211 return len(os.Args) < 2 || os.Args[1] != manager.MetadataSubcommandName 212 }