github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/cmd/docker/docker.go (about) 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "strconv" 8 "strings" 9 10 "github.com/docker/cli/cli" 11 "github.com/docker/cli/cli/command" 12 "github.com/docker/cli/cli/command/commands" 13 cliconfig "github.com/docker/cli/cli/config" 14 "github.com/docker/cli/cli/debug" 15 cliflags "github.com/docker/cli/cli/flags" 16 "github.com/docker/cli/internal/containerizedengine" 17 "github.com/docker/docker/api/types/versions" 18 "github.com/docker/docker/client" 19 "github.com/docker/docker/pkg/term" 20 "github.com/sirupsen/logrus" 21 "github.com/spf13/cobra" 22 "github.com/spf13/pflag" 23 ) 24 25 func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command { 26 opts := cliflags.NewClientOptions() 27 var flags *pflag.FlagSet 28 29 cmd := &cobra.Command{ 30 Use: "docker [OPTIONS] COMMAND [ARG...]", 31 Short: "A self-sufficient runtime for containers", 32 SilenceUsage: true, 33 SilenceErrors: true, 34 TraverseChildren: true, 35 Args: noArgs, 36 PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 37 // flags must be the top-level command flags, not cmd.Flags() 38 opts.Common.SetDefaultOptions(flags) 39 dockerPreRun(opts) 40 if err := dockerCli.Initialize(opts); err != nil { 41 return err 42 } 43 return isSupported(cmd, dockerCli) 44 }, 45 Version: fmt.Sprintf("%s, build %s", cli.Version, cli.GitCommit), 46 DisableFlagsInUseLine: true, 47 } 48 cli.SetupRootCommand(cmd) 49 50 flags = cmd.Flags() 51 flags.BoolP("version", "v", false, "Print version information and quit") 52 flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files") 53 opts.Common.InstallFlags(flags) 54 55 setFlagErrorFunc(dockerCli, cmd, flags, opts) 56 57 setHelpFunc(dockerCli, cmd, flags, opts) 58 59 cmd.SetOutput(dockerCli.Out()) 60 commands.AddCommands(cmd, dockerCli) 61 62 disableFlagsInUseLine(cmd) 63 setValidateArgs(dockerCli, cmd, flags, opts) 64 65 return cmd 66 } 67 68 func disableFlagsInUseLine(cmd *cobra.Command) { 69 visitAll(cmd, func(ccmd *cobra.Command) { 70 // do not add a `[flags]` to the end of the usage line. 71 ccmd.DisableFlagsInUseLine = true 72 }) 73 } 74 75 func setFlagErrorFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) { 76 // When invoking `docker stack --nonsense`, we need to make sure FlagErrorFunc return appropriate 77 // output if the feature is not supported. 78 // As above cli.SetupRootCommand(cmd) have already setup the FlagErrorFunc, we will add a pre-check before the FlagErrorFunc 79 // is called. 80 flagErrorFunc := cmd.FlagErrorFunc() 81 cmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { 82 if err := initializeDockerCli(dockerCli, flags, opts); err != nil { 83 return err 84 } 85 if err := isSupported(cmd, dockerCli); err != nil { 86 return err 87 } 88 return flagErrorFunc(cmd, err) 89 }) 90 } 91 92 func setHelpFunc(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) { 93 defaultHelpFunc := cmd.HelpFunc() 94 cmd.SetHelpFunc(func(ccmd *cobra.Command, args []string) { 95 if err := initializeDockerCli(dockerCli, flags, opts); err != nil { 96 ccmd.Println(err) 97 return 98 } 99 if err := isSupported(ccmd, dockerCli); err != nil { 100 ccmd.Println(err) 101 return 102 } 103 104 hideUnsupportedFeatures(ccmd, dockerCli) 105 defaultHelpFunc(ccmd, args) 106 }) 107 } 108 109 func setValidateArgs(dockerCli *command.DockerCli, cmd *cobra.Command, flags *pflag.FlagSet, opts *cliflags.ClientOptions) { 110 // The Args is handled by ValidateArgs in cobra, which does not allows a pre-hook. 111 // As a result, here we replace the existing Args validation func to a wrapper, 112 // where the wrapper will check to see if the feature is supported or not. 113 // The Args validation error will only be returned if the feature is supported. 114 visitAll(cmd, func(ccmd *cobra.Command) { 115 // if there is no tags for a command or any of its parent, 116 // there is no need to wrap the Args validation. 117 if !hasTags(ccmd) { 118 return 119 } 120 121 if ccmd.Args == nil { 122 return 123 } 124 125 cmdArgs := ccmd.Args 126 ccmd.Args = func(cmd *cobra.Command, args []string) error { 127 if err := initializeDockerCli(dockerCli, flags, opts); err != nil { 128 return err 129 } 130 if err := isSupported(cmd, dockerCli); err != nil { 131 return err 132 } 133 return cmdArgs(cmd, args) 134 } 135 }) 136 } 137 138 func initializeDockerCli(dockerCli *command.DockerCli, flags *pflag.FlagSet, opts *cliflags.ClientOptions) error { 139 if dockerCli.Client() != nil { 140 return nil 141 } 142 // when using --help, PersistentPreRun is not called, so initialization is needed. 143 // flags must be the top-level command flags, not cmd.Flags() 144 opts.Common.SetDefaultOptions(flags) 145 dockerPreRun(opts) 146 return dockerCli.Initialize(opts) 147 } 148 149 // visitAll will traverse all commands from the root. 150 // This is different from the VisitAll of cobra.Command where only parents 151 // are checked. 152 func visitAll(root *cobra.Command, fn func(*cobra.Command)) { 153 for _, cmd := range root.Commands() { 154 visitAll(cmd, fn) 155 } 156 fn(root) 157 } 158 159 func noArgs(cmd *cobra.Command, args []string) error { 160 if len(args) == 0 { 161 return nil 162 } 163 return fmt.Errorf( 164 "docker: '%s' is not a docker command.\nSee 'docker --help'", args[0]) 165 } 166 167 func main() { 168 // Set terminal emulation based on platform as required. 169 stdin, stdout, stderr := term.StdStreams() 170 logrus.SetOutput(stderr) 171 172 dockerCli := command.NewDockerCli(stdin, stdout, stderr, contentTrustEnabled(), containerizedengine.NewClient) 173 cmd := newDockerCommand(dockerCli) 174 175 if err := cmd.Execute(); err != nil { 176 if sterr, ok := err.(cli.StatusError); ok { 177 if sterr.Status != "" { 178 fmt.Fprintln(stderr, sterr.Status) 179 } 180 // StatusError should only be used for errors, and all errors should 181 // have a non-zero exit status, so never exit with 0 182 if sterr.StatusCode == 0 { 183 os.Exit(1) 184 } 185 os.Exit(sterr.StatusCode) 186 } 187 fmt.Fprintln(stderr, err) 188 os.Exit(1) 189 } 190 } 191 192 func contentTrustEnabled() bool { 193 if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { 194 if t, err := strconv.ParseBool(e); t || err != nil { 195 // treat any other value as true 196 return true 197 } 198 } 199 return false 200 } 201 202 func dockerPreRun(opts *cliflags.ClientOptions) { 203 cliflags.SetLogLevel(opts.Common.LogLevel) 204 205 if opts.ConfigDir != "" { 206 cliconfig.SetDir(opts.ConfigDir) 207 } 208 209 if opts.Common.Debug { 210 debug.Enable() 211 } 212 } 213 214 type versionDetails interface { 215 Client() client.APIClient 216 ClientInfo() command.ClientInfo 217 ServerInfo() command.ServerInfo 218 } 219 220 func hideFeatureFlag(f *pflag.Flag, hasFeature bool, annotation string) { 221 if hasFeature { 222 return 223 } 224 if _, ok := f.Annotations[annotation]; ok { 225 f.Hidden = true 226 } 227 } 228 229 func hideFeatureSubCommand(subcmd *cobra.Command, hasFeature bool, annotation string) { 230 if hasFeature { 231 return 232 } 233 if _, ok := subcmd.Annotations[annotation]; ok { 234 subcmd.Hidden = true 235 } 236 } 237 238 func hideUnsupportedFeatures(cmd *cobra.Command, details versionDetails) { 239 clientVersion := details.Client().ClientVersion() 240 osType := details.ServerInfo().OSType 241 hasExperimental := details.ServerInfo().HasExperimental 242 hasExperimentalCLI := details.ClientInfo().HasExperimental 243 244 cmd.Flags().VisitAll(func(f *pflag.Flag) { 245 hideFeatureFlag(f, hasExperimental, "experimental") 246 hideFeatureFlag(f, hasExperimentalCLI, "experimentalCLI") 247 // hide flags not supported by the server 248 if !isOSTypeSupported(f, osType) || !isVersionSupported(f, clientVersion) { 249 f.Hidden = true 250 } 251 // root command shows all top-level flags 252 if cmd.Parent() != nil { 253 if commands, ok := f.Annotations["top-level"]; ok { 254 f.Hidden = !findCommand(cmd, commands) 255 } 256 } 257 }) 258 259 for _, subcmd := range cmd.Commands() { 260 hideFeatureSubCommand(subcmd, hasExperimental, "experimental") 261 hideFeatureSubCommand(subcmd, hasExperimentalCLI, "experimentalCLI") 262 // hide subcommands not supported by the server 263 if subcmdVersion, ok := subcmd.Annotations["version"]; ok && versions.LessThan(clientVersion, subcmdVersion) { 264 subcmd.Hidden = true 265 } 266 if v, ok := subcmd.Annotations["ostype"]; ok && v != osType { 267 subcmd.Hidden = true 268 } 269 } 270 } 271 272 // Checks if a command or one of its ancestors is in the list 273 func findCommand(cmd *cobra.Command, commands []string) bool { 274 if cmd == nil { 275 return false 276 } 277 for _, c := range commands { 278 if c == cmd.Name() { 279 return true 280 } 281 } 282 return findCommand(cmd.Parent(), commands) 283 } 284 285 func isSupported(cmd *cobra.Command, details versionDetails) error { 286 if err := areSubcommandsSupported(cmd, details); err != nil { 287 return err 288 } 289 return areFlagsSupported(cmd, details) 290 } 291 292 func areFlagsSupported(cmd *cobra.Command, details versionDetails) error { 293 clientVersion := details.Client().ClientVersion() 294 osType := details.ServerInfo().OSType 295 hasExperimental := details.ServerInfo().HasExperimental 296 hasExperimentalCLI := details.ClientInfo().HasExperimental 297 298 errs := []string{} 299 300 cmd.Flags().VisitAll(func(f *pflag.Flag) { 301 if f.Changed { 302 if !isVersionSupported(f, clientVersion) { 303 errs = append(errs, fmt.Sprintf("\"--%s\" requires API version %s, but the Docker daemon API version is %s", f.Name, getFlagAnnotation(f, "version"), clientVersion)) 304 return 305 } 306 if !isOSTypeSupported(f, osType) { 307 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)) 308 return 309 } 310 if _, ok := f.Annotations["experimental"]; ok && !hasExperimental { 311 errs = append(errs, fmt.Sprintf("\"--%s\" is only supported on a Docker daemon with experimental features enabled", f.Name)) 312 } 313 if _, ok := f.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI { 314 errs = append(errs, fmt.Sprintf("\"--%s\" is on a Docker cli with experimental cli features enabled", f.Name)) 315 } 316 } 317 }) 318 if len(errs) > 0 { 319 return errors.New(strings.Join(errs, "\n")) 320 } 321 return nil 322 } 323 324 // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` 325 func areSubcommandsSupported(cmd *cobra.Command, details versionDetails) error { 326 clientVersion := details.Client().ClientVersion() 327 osType := details.ServerInfo().OSType 328 hasExperimental := details.ServerInfo().HasExperimental 329 hasExperimentalCLI := details.ClientInfo().HasExperimental 330 331 // Check recursively so that, e.g., `docker stack ls` returns the same output as `docker stack` 332 for curr := cmd; curr != nil; curr = curr.Parent() { 333 if cmdVersion, ok := curr.Annotations["version"]; ok && versions.LessThan(clientVersion, cmdVersion) { 334 return fmt.Errorf("%s requires API version %s, but the Docker daemon API version is %s", cmd.CommandPath(), cmdVersion, clientVersion) 335 } 336 if os, ok := curr.Annotations["ostype"]; ok && os != osType { 337 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) 338 } 339 if _, ok := curr.Annotations["experimental"]; ok && !hasExperimental { 340 return fmt.Errorf("%s is only supported on a Docker daemon with experimental features enabled", cmd.CommandPath()) 341 } 342 if _, ok := curr.Annotations["experimentalCLI"]; ok && !hasExperimentalCLI { 343 return fmt.Errorf("%s is only supported on a Docker cli with experimental cli features enabled", cmd.CommandPath()) 344 } 345 } 346 return nil 347 } 348 349 func getFlagAnnotation(f *pflag.Flag, annotation string) string { 350 if value, ok := f.Annotations[annotation]; ok && len(value) == 1 { 351 return value[0] 352 } 353 return "" 354 } 355 356 func isVersionSupported(f *pflag.Flag, clientVersion string) bool { 357 if v := getFlagAnnotation(f, "version"); v != "" { 358 return versions.GreaterThanOrEqualTo(clientVersion, v) 359 } 360 return true 361 } 362 363 func isOSTypeSupported(f *pflag.Flag, osType string) bool { 364 if v := getFlagAnnotation(f, "ostype"); v != "" && osType != "" { 365 return osType == v 366 } 367 return true 368 } 369 370 // hasTags return true if any of the command's parents has tags 371 func hasTags(cmd *cobra.Command) bool { 372 for curr := cmd; curr != nil; curr = curr.Parent() { 373 if len(curr.Annotations) > 0 { 374 return true 375 } 376 } 377 378 return false 379 }