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