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() {
// Set terminal emulation based on platform as required.
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()
//***输出version信息***//
if *flVersion {
showVersion()
return
}
//***输出help信息***//
if *flHelp {
// if global flag --help is present, regardless of what other options and commands there are,
// just print the usage.
flag.Usage()
return
}
//***生成clientCli,client.NewDockerCli()定义在/api/client/cli.go中***//
clientCli := client.NewDockerCli(stdin, stdout, stderr, clientFlags)
//***New()定义在/cli/cli.go中***//
c := cli.New(clientCli, NewDaemonProxy(), cobraAdaptor)
//***Run()定义在/cli/cli.go中***//
if err := c.Run(flag.Args()...); err != nil {
if sterr, ok := err.(cli.StatusError); ok {
if sterr.Status != "" {
fmt.Fprintln(stderr, sterr.Status)
}
// StatusError should only be used for errors, and all errors should
// have a non-zero exit status, so never exit with 0
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
//***生成dockerclient***//
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)
//***调用NewAPIClientFromFlags(),依据flag,生成client***//
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
// NewAPIClientFromFlags creates a new APIClient from command line flags
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
}
//***client.NewClient()定义在/vendor/src/github.com/docker/engine-api/client/client.go中***//
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
// Command returns a cli command handler if one exists
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"
// DaemonProxy acts as a cli.Handler to proxy calls to the daemon binary
type DaemonProxy struct{}
// NewDaemonProxy returns a new handler
func NewDaemonProxy() DaemonProxy {
return DaemonProxy{}
}
// Command returns a cli command handler if one exists
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
// CobraAdaptor is an adaptor for supporting spf13/cobra commands in the
// docker/cli framework
type CobraAdaptor struct {
rootCmd *cobra.Command
dockerCli *client.DockerCli
}
//***生成新的CobraAdaptor,并向其注册cmd***//
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
//***找到合适的cmd执行函数,返回的是函数***//
func (c CobraAdaptor) Command(name string) func(...string) error {
for _, cmd := range c.rootCmd.Commands() {
//***Name()返回的是cmd中的use中的内容***//
if cmd.Name() == name {
return func(args ...string) error {
return c.run(name, args)
}
}
}
return nil
}
//***执行相应的cmd处理函数***//
func (c CobraAdaptor) run(cmd string, args []string) error {
if err := c.dockerCli.Initialize(); err != nil {
return err
}
// Prepend the command name to support normal cobra command delegation
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
//***Cli是对多个handler的封装***//
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
//***Cli入口***//
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 {
//***如docker ps等命令真正执行的地方***//
//***获取处理函数***//
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
//***根据子命令找到对应的cmd处理函数***//
func (cli *Cli) command(args ...string) (func(...string) error, error) {
for _, c := range cli.handlers {
if c == nil {
continue
}
//***container, image等相关的cmd处理函数由cobraadaptor负责,其Command()函数定义在/cli/cobraadaptor/adaptor.go中***//
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包来管理命令行那就更好了。