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