Docker命令行
在使用Docker时,我们通过docker run等命令和dockerd进行交互。目前Docker分为docker和dockerd两个二进制文件,其中docker为客户端;dockerd为server端。本次分析将介绍docker程序是如何解析命令行的。
docker程序入口
docker程序的入口在/cmd/docker/docker.go中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| func main() { stdin, stdout, stderr := term.StdStreams() logrus.SetOutput(stderr) flag.Merge(flag.CommandLine, clientFlags.FlagSet, commonFlags.FlagSet) cobraAdaptor := cobraadaptor.NewCobraAdaptor(clientFlags) flag.Usage = func() { fmt.Fprint(stdout, "Usage: docker [OPTIONS] COMMAND [arg...]\n docker [ --help | -v | --version ]\n\n") fmt.Fprint(stdout, "A self-sufficient runtime for containers.\n\nOptions:\n") flag.CommandLine.SetOutput(stdout) flag.PrintDefaults() help := "\nCommands:\n" dockerCommands := append(cli.DockerCommandUsage, cobraAdaptor.Usage()...) for _, cmd := range sortCommands(dockerCommands) { help += fmt.Sprintf(" %-10.10s%s\n", cmd.Name, cmd.Description) } help += "\nRun 'docker COMMAND --help' for more information on a command." fmt.Fprintf(stdout, "%s\n", help) } flag.Parse() if *flVersion { showVersion() return } if *flHelp { flag.Usage() return } clientCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) c := cli.New(clientCli, NewDaemonProxy(), cobraAdaptor) if err := c.Run(flag.Args()...); err != nil { if sterr, ok := err.(cli.StatusError); ok { if sterr.Status != "" { fmt.Fprintln(stderr, sterr.Status) } if sterr.StatusCode == 0 { os.Exit(1) } os.Exit(sterr.StatusCode) } fmt.Fprintln(stderr, err) os.Exit(1) } }
|
在main()函数中,如果参数为-h或–help,则调用Usage()打印帮助信息;如果参数为-v或–version,则调用showVersion()打印版本信息;否则调用Cli的Run()方法来运行具体命令。
可以看出,cli.New(clientCli, NewDaemonProxy(), cobraAdaptor)生成Cli,而clientCli, NewDaemonProxy(), cobraAdaptor都称为handler,所以可以把Cli看成是对多个handler的封装。
clientCli
在main()中,clientCli通过
clientCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags)
生成。
到/api/client/cli.go中来看下NewDockerCli()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| func NewDockerCli(in io.ReadCloser, out, err io.Writer, clientFlags *cliflags.ClientFlags) *DockerCli { cli := &DockerCli{ in: in, out: out, err: err, keyFile: clientFlags.Common.TrustKey, } cli.init = func() error { clientFlags.PostParse() cli.configFile = LoadDefaultConfigFile(err) client, err := NewAPIClientFromFlags(clientFlags, cli.configFile) if err != nil { return err } cli.client = client if cli.in != nil { cli.inFd, cli.isTerminalIn = term.GetFdInfo(cli.in) } if cli.out != nil { cli.outFd, cli.isTerminalOut = term.GetFdInfo(cli.out) } return nil } return cli }
|
NewAPIClientFromFlags()定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| func NewAPIClientFromFlags(clientFlags *cliflags.ClientFlags, configFile *configfile.ConfigFile) (client.APIClient, error) { host, err := getServerHost(clientFlags.Common.Hosts, clientFlags.Common.TLSOptions) if err != nil { return &client.Client{}, err } customHeaders := configFile.HTTPHeaders if customHeaders == nil { customHeaders = map[string]string{} } customHeaders["User-Agent"] = clientUserAgent() verStr := api.DefaultVersion if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" { verStr = tmpStr } httpClient, err := newHTTPClient(host, clientFlags.Common.TLSOptions) if err != nil { return &client.Client{}, err } return client.NewClient(host, verStr, httpClient, customHeaders) }
|
所以,可以看出clientCli包含了一个与dockerd打交道的docker client。
那么clientCli怎么可以成为一个handler呢?要知道handler必须实现Command()方法。clientCli的Command()方法定义在/api/client/commands.go中:
1 2 3 4 5 6 7 8 9
| func (cli *DockerCli) Command(name string) func(...string) error { return map[string]func(...string) error{ "exec": cli.CmdExec, "info": cli.CmdInfo, "inspect": cli.CmdInspect, "update": cli.CmdUpdate, }[name] }
|
所以,clientCli handler会处理exec, info, inspect, update四个子命令。clientCli可以通过Command()方法返回合适的命令处理函数。
NewDaemonProxy()
NewDaemonProxy()定义在/cmd/docker/daemon.go中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const daemonBinary = "dockerd" type DaemonProxy struct{} func NewDaemonProxy() DaemonProxy { return DaemonProxy{} } func (p DaemonProxy) Command(name string) func(...string) error { return map[string]func(...string) error{ "daemon": p.CmdDaemon, }[name] }
|
可以看出,DaemonProxy是为了兼容旧式”docker daemon”的启动方式,其本质还是调用了dockerd程序。
cobraAdaptor
cobraAdaptor集成了大部分的docker命令。来看来其生成函数NewCobraAdaptor(),定义在/cli/cobraadaptor/adaptor.go中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| type CobraAdaptor struct { rootCmd *cobra.Command dockerCli *client.DockerCli } func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { stdin, stdout, stderr := term.StdStreams() dockerCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags) var rootCmd = &cobra.Command{ Use: "docker [OPTIONS]", Short: "A self-sufficient runtime for containers", SilenceUsage: true, SilenceErrors: true, } rootCmd.SetUsageTemplate(usageTemplate) rootCmd.SetHelpTemplate(helpTemplate) rootCmd.SetFlagErrorFunc(cli.FlagErrorFunc) rootCmd.SetOutput(stdout) rootCmd.AddCommand( node.NewNodeCommand(dockerCli), service.NewServiceCommand(dockerCli), stack.NewStackCommand(dockerCli), stack.NewTopLevelDeployCommand(dockerCli), swarm.NewSwarmCommand(dockerCli), container.NewAttachCommand(dockerCli), container.NewCommitCommand(dockerCli), container.NewCopyCommand(dockerCli), container.NewCreateCommand(dockerCli), container.NewDiffCommand(dockerCli), container.NewExportCommand(dockerCli), container.NewKillCommand(dockerCli), container.NewLogsCommand(dockerCli), container.NewPauseCommand(dockerCli), container.NewPortCommand(dockerCli), container.NewPsCommand(dockerCli), container.NewRenameCommand(dockerCli), container.NewRestartCommand(dockerCli), container.NewRmCommand(dockerCli), container.NewRunCommand(dockerCli), container.NewStartCommand(dockerCli), container.NewStatsCommand(dockerCli), container.NewStopCommand(dockerCli), container.NewTopCommand(dockerCli), container.NewUnpauseCommand(dockerCli), container.NewWaitCommand(dockerCli), image.NewBuildCommand(dockerCli), image.NewHistoryCommand(dockerCli), image.NewImagesCommand(dockerCli), image.NewLoadCommand(dockerCli), image.NewRemoveCommand(dockerCli), image.NewSaveCommand(dockerCli), image.NewPullCommand(dockerCli), image.NewPushCommand(dockerCli), image.NewSearchCommand(dockerCli), image.NewImportCommand(dockerCli), image.NewTagCommand(dockerCli), network.NewNetworkCommand(dockerCli), system.NewEventsCommand(dockerCli), registry.NewLoginCommand(dockerCli), registry.NewLogoutCommand(dockerCli), system.NewVersionCommand(dockerCli), volume.NewVolumeCommand(dockerCli), ) plugin.NewPluginCommand(rootCmd, dockerCli) rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") rootCmd.PersistentFlags().MarkShorthandDeprecated("help", "please use --help") return CobraAdaptor{ rootCmd: rootCmd, dockerCli: dockerCli, } }
|
可以看出,CobraAdaptor使用了spf13/cobra命令行管理包。所有的命令都以docker子命令的形式进行管理。
再来看下CobraAdaptor的Command()方法是如何返回合适的方法的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| func (c CobraAdaptor) Command(name string) func(...string) error { for _, cmd := range c.rootCmd.Commands() { if cmd.Name() == name { return func(args ...string) error { return c.run(name, args) } } } return nil } func (c CobraAdaptor) run(cmd string, args []string) error { if err := c.dockerCli.Initialize(); err != nil { return err } c.rootCmd.SetArgs(append([]string{cmd}, args...)) return c.rootCmd.Execute() }
|
可以看出,Command()方法会从子命令中找到合适的命令,然后执行”docker”+子命令+参数的函数。当然,Command()返回的是一个函数。
Cli
Cli定义在/cli/cli.go中:
1 2 3 4 5 6
| type Cli struct { Stderr io.Writer handlers []Handler Usage func() }
|
可以看出,Cli封装有多个Handler。再来看Cli的入口,Run()方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| func (cli *Cli) Run(args ...string) error { if len(args) > 1 { command, err := cli.command(args[:2]...) if err == nil { return command(args[2:]...) } if err != errCommandNotFound { return err } } if len(args) > 0 { command, err := cli.command(args[0]) if err != nil { if err == errCommandNotFound { cli.noSuchCommand(args[0]) return nil } return err } return command(args[1:]...) } return cli.CmdHelp() }
|
Run()方法会区分命令中参数的个数,如”docker network create”,则走len(args)>1分支;如”docker ps”, 则走len(args)>0的分支;如果都不符合,则打印帮助信息。
Cli通过command()方法获取合适的方法。来看command()的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| func (cli *Cli) command(args ...string) (func(...string) error, error) { for _, c := range cli.handlers { if c == nil { continue } if cmd := c.Command(strings.Join(args, " ")); cmd != nil { if ci, ok := c.(Initializer); ok { if err := ci.Initialize(); err != nil { return nil, err } } return cmd, nil } } return nil, errCommandNotFound }
|
command()方法很好理解,轮询Cli中每一个handler,直到有一个handler可以处理该命令行。handler可以处理该命令行,即通过handler的Command()方法可以返回一个正确的函数,而之前介绍过,系统中的三个handler都实现有Command()方法返回命令行对应的处理函数。
总结
本次分析了docker的命令行形式。其通过Cli封装了handler,handler实现有Command()方法来返回一个合适的处理函数。这与老版本的docker命令行管理方法来说更进了一步。老版本(v1.8.3)中是通过参数,然后直接通过reflect的GetMethodByName()方法获取处理函数并返回。
当然,如果可以直接使用spf13/cobra包来管理命令行那就更好了。