github.com/itscaro/cli@v0.0.0-20190705081621-c9db0fe93829/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.SetOutput(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 hideFeatureFlag(f *pflag.Flag, hasFeature bool, annotation string) { 316 if hasFeature { 317 return 318 } 319 if _, ok := f.Annotations[annotation]; ok { 320 f.Hidden = true 321 } 322 } 323 324 func hideFeatureSubCommand(subcmd *cobra.Command, hasFeature bool, annotation string) { 325 if hasFeature { 326 return 327 } 328 if _, ok := subcmd.Annotations[annotation]; ok { 329 subcmd.Hidden = true 330 } 331 } 332 333 func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) error { 334 clientVersion := details.Client().ClientVersion() 335 osType := details.ServerInfo().OSType 336 hasExperimental := details.ServerInfo().HasExperimental 337 hasExperimentalCLI := details.ClientInfo().HasExperimental 338 hasBuildKit, err := command.BuildKitEnabled(details.ServerInfo()) 339 if err != nil { 340 return err 341 } 342 343 cmd.Flags().VisitAll(func(f *pflag.Flag) { 344 hideFeatureFlag(f, hasExperimental, "experimental") 345 hideFeatureFlag(f, hasExperimentalCLI, "experimentalCLI") 346 hideFeatureFlag(f, hasBuildKit, "buildkit") 347 hideFeatureFlag(f, !hasBuildKit, "no-buildkit") 348 // hide flags not supported by the server 349 if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) { 350 f.Hidden = true 351 } 352 // root command shows all top-level flags 353 if cmd.Parent() != nil { 354 if commands, ok := f.Annotations["top-level"]; ok { 355 f.Hidden = !findCommand(cmd, commands) 356 } 357 } 358 }) 359 360 for _, subcmd := range cmd.Commands() { 361 hideFeatureSubCommand(subcmd, hasExperimental, "experimental") 362 hideFeatureSubCommand(subcmd, hasExperimentalCLI, "experimentalCLI") 363 hideFeatureSubCommand(subcmd, hasBuildKit, "buildkit") 364 hideFeatureSubCommand(subcmd, !hasBuildKit, "no-buildkit") 365 // hide subcommands not supported by the server 366 if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) { 367 subcmd.Hidden = true 368 } 369 if v, ok := subcmd.Annotations["ostype"]; ok && v != osType { 370 subcmd.Hidden = true 371 } 372 } 373 return nil 374 } 375 376 // Checks if a command or one of its ancestors is in the list 377 func findCommand(cmd *cobra.Command, commands []string) bool { 378 if cmd == nil { 379 return false 380 } 381 for _, c := range commands { 382 if c == cmd.Name() { 383 return true 384 } 385 } 386 return findCommand(cmd.Parent(), commands) 387 } 388 389 func isSupported(cmd *cobra.Command, details versionDetails) error { 390 if err := areSubcommandsSupported(cmd, details); err != nil { 391 return err 392 } 393 return areFlagsSupported(cmd, details) 394 } 395 396 func areFlagsSupported(cmd *cobra.Command, details versionDetails) error { 397 clientVersion := details.Client().ClientVersion() 398 osType := details.ServerInfo().OSType 399 hasExperimental := details.ServerInfo().HasExperimental 400 hasExperimentalCLI := details.ClientInfo().HasExperimental 401 402 errs := []string{} 403 404 cmd.Flags().VisitAll(func(f *pflag.Flag) { 405 if f.Changed { 406 if !isVersionSupported(f, clientVersion) { 407 errs = append(errs, fmt.Sprintf("\"--%s\" requires API version %s, but the Docker daemon API version is %s", f.Name, getFlagAnnotation(f, "version"), clientVersion)) 408 return 409 } 410 if !isOSTypeSupported(f, osType) { 411 errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker daemon running on %s, but the Docker daemon is running on %s", f.Name, getFlagAnnotation(f, "ostype"), osType)) 412 return 413 } 414 if _, ok := f.Annotations["experimental"]; ok && !hasExperimental { 415 errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker daemon with experimental features enabled", f.Name)) 416 } 417 if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI { 418 errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker cli with experimental cli features enabled", f.Name)) 419 } 420 // buildkit-specific flags are noop when buildkit is not enabled, so we do not add an error in that case 421 } 422 }) 423 if len(errs) > 0 { 424 return errors.New(strings.Join(errs, "\n")) 425 } 426 return nil 427 } 428 429 // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` 430 func areSubcommandsSupported(cmd *cobra.Command, details versionDetails) error { 431 clientVersion := details.Client().ClientVersion() 432 osType := details.ServerInfo().OSType 433 hasExperimental := details.ServerInfo().HasExperimental 434 hasExperimentalCLI := details.ClientInfo().HasExperimental 435 436 // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` 437 for curr := cmd; curr != nil; curr = curr.Parent() { 438 if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(clientVersion, cmdVersion) { 439 return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, clientVersion) 440 } 441 if os, ok := curr.Annotations["ostype"]; ok && os != osType { 442 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, osType) 443 } 444 if _, ok := curr.Annotations["experimental"]; ok && !hasExperimental { 445 return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath()) 446 } 447 if _, ok := curr.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI { 448 return fmt.Errorf("%s is only supported on a Docker cli with experimental cli features enabled", cmd.CommandPath()) 449 } 450 } 451 return nil 452 } 453 454 func getFlagAnnotation(f *pflag.Flag, annotation string) string { 455 if value, ok := f.Annotations[annotation]; ok && len(value) == 1 { 456 return value[0] 457 } 458 return "" 459 } 460 461 func isVersionSupported(f *pflag.Flag, clientVersion string) bool { 462 if v := getFlagAnnotation(f, "version"); v != "" { 463 return versions.GreaterThanOrEqualTo(clientVersion, v) 464 } 465 return true 466 } 467 468 func isOSTypeSupported(f *pflag.Flag, osType string) bool { 469 if v := getFlagAnnotation(f, "ostype"); v != "" && osType != "" { 470 return osType == v 471 } 472 return true 473 } 474 475 // hasTags return true if any of the command's parents has tags 476 func hasTags(cmd *cobra.Command) bool { 477 for curr := cmd; curr != nil; curr = curr.Parent() { 478 if len(curr.Annotations) > 0 { 479 return true 480 } 481 } 482 483 return false 484 }