github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cmd/docker/docker.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "os" 6 "os/exec" 7 "strings" 8 "syscall" 9 10 "github.com/docker/cli/cli" 11 pluginmanager "github.com/docker/cli/cli-plugins/manager" 12 "github.com/docker/cli/cli/command" 13 "github.com/docker/cli/cli/command/commands" 14 cliflags "github.com/docker/cli/cli/flags" 15 "github.com/docker/cli/cli/version" 16 "github.com/docker/docker/api/types/versions" 17 "github.com/moby/buildkit/util/appcontext" 18 "github.com/pkg/errors" 19 "github.com/sirupsen/logrus" 20 "github.com/spf13/cobra" 21 "github.com/spf13/pflag" 22 ) 23 24 func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand { 25 var ( 26 opts *cliflags.ClientOptions 27 flags *pflag.FlagSet 28 helpCmd *cobra.Command 29 ) 30 31 cmd := &cobra.Command{ 32 Use: "docker [OPTIONS] COMMAND [ARG...]", 33 Short: "A self-sufficient runtime for containers", 34 SilenceUsage: true, 35 SilenceErrors: true, 36 TraverseChildren: true, 37 RunE: func(cmd *cobra.Command, args []string) error { 38 if len(args) == 0 { 39 return command.ShowHelp(dockerCli.Err())(cmd, args) 40 } 41 return fmt.Errorf("docker: '%s' is not a docker command.\nSee 'docker --help'", args[0]) 42 }, 43 PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 44 return isSupported(cmd, dockerCli) 45 }, 46 Version: fmt.Sprintf("%s, build %s", version.Version, version.GitCommit), 47 DisableFlagsInUseLine: true, 48 CompletionOptions: cobra.CompletionOptions{ 49 DisableDefaultCmd: false, 50 HiddenDefaultCmd: true, 51 DisableDescriptions: true, 52 }, 53 } 54 cmd.SetIn(dockerCli.In()) 55 cmd.SetOut(dockerCli.Out()) 56 cmd.SetErr(dockerCli.Err()) 57 58 opts, flags, helpCmd = cli.SetupRootCommand(cmd) 59 registerCompletionFuncForGlobalFlags(dockerCli, cmd) 60 flags.BoolP("version", "v", false, "Print version information and quit") 61 setFlagErrorFunc(dockerCli, cmd) 62 63 setupHelpCommand(dockerCli, cmd, helpCmd) 64 setHelpFunc(dockerCli, cmd) 65 66 cmd.SetOut(dockerCli.Out()) 67 commands.AddCommands(cmd, dockerCli) 68 69 cli.DisableFlagsInUseLine(cmd) 70 setValidateArgs(dockerCli, cmd) 71 72 // flags must be the top-level command flags, not cmd.Flags() 73 return cli.NewTopLevelCommand(cmd, dockerCli, opts, flags) 74 } 75 76 func setFlagErrorFunc(dockerCli command.Cli, cmd *cobra.Command) { 77 // When invoking `docker stack --nonsense`, we need to make sure FlagErrorFunc return appropriate 78 // output if the feature is not supported. 79 // As above cli.SetupRootCommand(cmd) have already setup the FlagErrorFunc, we will add a pre-check before the FlagErrorFunc 80 // is called. 81 flagErrorFunc := cmd.FlagErrorFunc() 82 cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { 83 if err := pluginmanager.AddPluginCommandStubs(dockerCli, cmd.Root()); err != nil { 84 return err 85 } 86 if err := isSupported(cmd, dockerCli); err != nil { 87 return err 88 } 89 if err := hideUnsupportedFeatures(cmd, dockerCli); err != nil { 90 return err 91 } 92 return flagErrorFunc(cmd, err) 93 }) 94 } 95 96 func setupHelpCommand(dockerCli command.Cli, rootCmd, helpCmd *cobra.Command) { 97 origRun := helpCmd.Run 98 origRunE := helpCmd.RunE 99 100 helpCmd.Run = nil 101 helpCmd.RunE = func(c *cobra.Command, args []string) error { 102 if len(args) > 0 { 103 helpcmd, err := pluginmanager.PluginRunCommand(dockerCli, args[0], rootCmd) 104 if err == nil { 105 return helpcmd.Run() 106 } 107 if !pluginmanager.IsNotFound(err) { 108 return errors.Errorf("unknown help topic: %v", strings.Join(args, " ")) 109 } 110 } 111 if origRunE != nil { 112 return origRunE(c, args) 113 } 114 origRun(c, args) 115 return nil 116 } 117 } 118 119 func tryRunPluginHelp(dockerCli command.Cli, ccmd *cobra.Command, cargs []string) error { 120 root := ccmd.Root() 121 122 cmd, _, err := root.Traverse(cargs) 123 if err != nil { 124 return err 125 } 126 helpcmd, err := pluginmanager.PluginRunCommand(dockerCli, cmd.Name(), root) 127 if err != nil { 128 return err 129 } 130 return helpcmd.Run() 131 } 132 133 func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) { 134 defaultHelpFunc := cmd.HelpFunc() 135 cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { 136 if pluginmanager.IsPluginCommand(ccmd) { 137 err := tryRunPluginHelp(dockerCli, ccmd, args) 138 if !pluginmanager.IsNotFound(err) { 139 ccmd.Println(err) 140 } 141 cmd.PrintErrf("unknown help topic: %v\n", ccmd.Name()) 142 return 143 } 144 145 if err := isSupported(ccmd, dockerCli); err != nil { 146 ccmd.Println(err) 147 return 148 } 149 if err := hideUnsupportedFeatures(ccmd, dockerCli); err != nil { 150 ccmd.Println(err) 151 return 152 } 153 154 defaultHelpFunc(ccmd, args) 155 }) 156 } 157 158 func setValidateArgs(dockerCli command.Cli, cmd *cobra.Command) { 159 // The Args is handled by ValidateArgs in cobra, which does not allows a pre-hook. 160 // As a result, here we replace the existing Args validation func to a wrapper, 161 // where the wrapper will check to see if the feature is supported or not. 162 // The Args validation error will only be returned if the feature is supported. 163 cli.VisitAll(cmd, func(ccmd *cobra.Command) { 164 // if there is no tags for a command or any of its parent, 165 // there is no need to wrap the Args validation. 166 if !hasTags(ccmd) { 167 return 168 } 169 170 if ccmd.Args == nil { 171 return 172 } 173 174 cmdArgs := ccmd.Args 175 ccmd.Args = func(cmd *cobra.Command, args []string) error { 176 if err := isSupported(cmd, dockerCli); err != nil { 177 return err 178 } 179 return cmdArgs(cmd, args) 180 } 181 }) 182 } 183 184 func tryPluginRun(dockerCli command.Cli, cmd *cobra.Command, subcommand string, envs []string) error { 185 plugincmd, err := pluginmanager.PluginRunCommand(dockerCli, subcommand, cmd) 186 if err != nil { 187 return err 188 } 189 plugincmd.Env = append(envs, plugincmd.Env...) 190 191 go func() { 192 // override SIGTERM handler so we let the plugin shut down first 193 <-appcontext.Context().Done() 194 }() 195 196 if err := plugincmd.Run(); err != nil { 197 statusCode := 1 198 exitErr, ok := err.(*exec.ExitError) 199 if !ok { 200 return err 201 } 202 if ws, ok := exitErr.Sys().(syscall.WaitStatus); ok { 203 statusCode = ws.ExitStatus() 204 } 205 return cli.StatusError{ 206 StatusCode: statusCode, 207 } 208 } 209 return nil 210 } 211 212 func runDocker(dockerCli *command.DockerCli) error { 213 tcmd := newDockerCommand(dockerCli) 214 215 cmd, args, err := tcmd.HandleGlobalFlags() 216 if err != nil { 217 return err 218 } 219 220 if err := tcmd.Initialize(); err != nil { 221 return err 222 } 223 224 var envs []string 225 args, os.Args, envs, err = processAliases(dockerCli, cmd, args, os.Args) 226 if err != nil { 227 return err 228 } 229 230 err = pluginmanager.AddPluginCommandStubs(dockerCli, cmd) 231 if err != nil { 232 return err 233 } 234 235 if len(args) > 0 { 236 ccmd, _, err := cmd.Find(args) 237 if err != nil || pluginmanager.IsPluginCommand(ccmd) { 238 err := tryPluginRun(dockerCli, cmd, args[0], envs) 239 if !pluginmanager.IsNotFound(err) { 240 return err 241 } 242 // For plugin not found we fall through to 243 // cmd.Execute() which deals with reporting 244 // "command not found" in a consistent way. 245 } 246 } 247 248 // We've parsed global args already, so reset args to those 249 // which remain. 250 cmd.SetArgs(args) 251 return cmd.Execute() 252 } 253 254 func main() { 255 dockerCli, err := command.NewDockerCli() 256 if err != nil { 257 fmt.Fprintln(os.Stderr, err) 258 os.Exit(1) 259 } 260 logrus.SetOutput(dockerCli.Err()) 261 262 if err := runDocker(dockerCli); err != nil { 263 if sterr, ok := err.(cli.StatusError); ok { 264 if sterr.Status != "" { 265 fmt.Fprintln(dockerCli.Err(), sterr.Status) 266 } 267 // StatusError should only be used for errors, and all errors should 268 // have a non-zero exit status, so never exit with 0 269 if sterr.StatusCode == 0 { 270 os.Exit(1) 271 } 272 os.Exit(sterr.StatusCode) 273 } 274 fmt.Fprintln(dockerCli.Err(), err) 275 os.Exit(1) 276 } 277 } 278 279 type versionDetails interface { 280 CurrentVersion() string 281 ServerInfo() command.ServerInfo 282 } 283 284 func hideFlagIf(f *pflag.Flag, condition func(string) bool, annotation string) { 285 if f.Hidden { 286 return 287 } 288 var val string 289 if values, ok := f.Annotations[annotation]; ok { 290 if len(values) > 0 { 291 val = values[0] 292 } 293 if condition(val) { 294 f.Hidden = true 295 } 296 } 297 } 298 299 func hideSubcommandIf(subcmd *cobra.Command, condition func(string) bool, annotation string) { 300 if subcmd.Hidden { 301 return 302 } 303 if v, ok := subcmd.Annotations[annotation]; ok { 304 if condition(v) { 305 subcmd.Hidden = true 306 } 307 } 308 } 309 310 func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) error { 311 var ( 312 notExperimental = func(_ string) bool { return !details.ServerInfo().HasExperimental } 313 notOSType = func(v string) bool { return details.ServerInfo().OSType != "" && v != details.ServerInfo().OSType } 314 notSwarmStatus = func(v string) bool { 315 s := details.ServerInfo().SwarmStatus 316 if s == nil { 317 // engine did not return swarm status header 318 return false 319 } 320 switch v { 321 case "manager": 322 // requires the node to be a manager 323 return !s.ControlAvailable 324 case "active": 325 // requires swarm to be active on the node (e.g. for swarm leave) 326 // only hide the command if we're sure the node is "inactive" 327 // for any other status, assume the "leave" command can still 328 // be used. 329 return s.NodeState == "inactive" 330 case "": 331 // some swarm commands, such as "swarm init" and "swarm join" 332 // are swarm-related, but do not require swarm to be active 333 return false 334 default: 335 // ignore any other value for the "swarm" annotation 336 return false 337 } 338 } 339 versionOlderThan = func(v string) bool { return versions.LessThan(details.CurrentVersion(), v) } 340 ) 341 342 cmd.Flags().VisitAll(func(f *pflag.Flag) { 343 // hide flags not supported by the server 344 // root command shows all top-level flags 345 if cmd.Parent() != nil { 346 if cmds, ok := f.Annotations["top-level"]; ok { 347 f.Hidden = !findCommand(cmd, cmds) 348 } 349 if f.Hidden { 350 return 351 } 352 } 353 354 hideFlagIf(f, notExperimental, "experimental") 355 hideFlagIf(f, notOSType, "ostype") 356 hideFlagIf(f, notSwarmStatus, "swarm") 357 hideFlagIf(f, versionOlderThan, "version") 358 }) 359 360 for _, subcmd := range cmd.Commands() { 361 hideSubcommandIf(subcmd, notExperimental, "experimental") 362 hideSubcommandIf(subcmd, notOSType, "ostype") 363 hideSubcommandIf(subcmd, notSwarmStatus, "swarm") 364 hideSubcommandIf(subcmd, versionOlderThan, "version") 365 } 366 return nil 367 } 368 369 // Checks if a command or one of its ancestors is in the list 370 func findCommand(cmd *cobra.Command, commands []string) bool { 371 if cmd == nil { 372 return false 373 } 374 for _, c := range commands { 375 if c == cmd.Name() { 376 return true 377 } 378 } 379 return findCommand(cmd.Parent(), commands) 380 } 381 382 func isSupported(cmd *cobra.Command, details versionDetails) error { 383 if err := areSubcommandsSupported(cmd, details); err != nil { 384 return err 385 } 386 return areFlagsSupported(cmd, details) 387 } 388 389 func areFlagsSupported(cmd *cobra.Command, details versionDetails) error { 390 errs := []string{} 391 392 cmd.Flags().VisitAll(func(f *pflag.Flag) { 393 if !f.Changed { 394 return 395 } 396 if !isVersionSupported(f, details.CurrentVersion()) { 397 errs = append(errs, fmt.Sprintf(`"--%s" requires API version %s, but the Docker daemon API version is %s`, f.Name, getFlagAnnotation(f, "version"), details.CurrentVersion())) 398 return 399 } 400 if !isOSTypeSupported(f, details.ServerInfo().OSType) { 401 errs = append(errs, fmt.Sprintf( 402 `"--%s" is only supported on a Docker daemon running on %s, but the Docker daemon is running on %s`, 403 f.Name, 404 getFlagAnnotation(f, "ostype"), details.ServerInfo().OSType), 405 ) 406 return 407 } 408 if _, ok := f.Annotations["experimental"]; ok && !details.ServerInfo().HasExperimental { 409 errs = append(errs, fmt.Sprintf(`"--%s" is only supported on a Docker daemon with experimental features enabled`, f.Name)) 410 } 411 // buildkit-specific flags are noop when buildkit is not enabled, so we do not add an error in that case 412 }) 413 if len(errs) > 0 { 414 return errors.New(strings.Join(errs, "\n")) 415 } 416 return nil 417 } 418 419 // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` 420 func areSubcommandsSupported(cmd *cobra.Command, details versionDetails) error { 421 // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` 422 for curr := cmd; curr != nil; curr = curr.Parent() { 423 // Important: in the code below, calls to "details.CurrentVersion()" and 424 // "details.ServerInfo()" are deliberately executed inline to make them 425 // be executed "lazily". This is to prevent making a connection with the 426 // daemon to perform a "ping" (even for commands that do not require a 427 // daemon connection). 428 // 429 // See commit b39739123b845f872549e91be184cc583f5b387c for details. 430 431 if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(details.CurrentVersion(), cmdVersion) { 432 return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, details.CurrentVersion()) 433 } 434 if ost, ok := curr.Annotations["ostype"]; ok && details.ServerInfo().OSType != "" && ost != details.ServerInfo().OSType { 435 return fmt.Errorf("%s is only supported on a Docker daemon running on %s, but the Docker daemon is running on %s", cmd.CommandPath(), ost, details.ServerInfo().OSType) 436 } 437 if _, ok := curr.Annotations["experimental"]; ok && !details.ServerInfo().HasExperimental { 438 return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath()) 439 } 440 } 441 return nil 442 } 443 444 func getFlagAnnotation(f *pflag.Flag, annotation string) string { 445 if value, ok := f.Annotations[annotation]; ok && len(value) == 1 { 446 return value[0] 447 } 448 return "" 449 } 450 451 func isVersionSupported(f *pflag.Flag, clientVersion string) bool { 452 if v := getFlagAnnotation(f, "version"); v != "" { 453 return versions.GreaterThanOrEqualTo(clientVersion, v) 454 } 455 return true 456 } 457 458 func isOSTypeSupported(f *pflag.Flag, osType string) bool { 459 if v := getFlagAnnotation(f, "ostype"); v != "" && osType != "" { 460 return osType == v 461 } 462 return true 463 } 464 465 // hasTags return true if any of the command's parents has tags 466 func hasTags(cmd *cobra.Command) bool { 467 for curr := cmd; curr != nil; curr = curr.Parent() { 468 if len(curr.Annotations) > 0 { 469 return true 470 } 471 } 472 473 return false 474 }