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