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