docker源码分析(一)-cli

Docker代码更新很快,网上各位大神的源码解析很多已经是几年前的版本了。实现上有了很大改变,加之Docker项目更名为moby,其中很多组件又从moby中拆分了出来,一开始看简直是一脸懵逼啊。在这里跟大家分享了一下最近看的docker/cli的源码,抛砖引玉,欢迎大家批评指正。

分析的docker client版本是17.06.2-ce

docker client是Docker的客户端程序,也就是我们敲的docker * * 命令,我们通过他与docker deamon程序进行交互。可以将他看成一个普通的客户端程序,docker的核心namespacecgroup等技术都不在这里。

搭建docker-client开发环境

docker_client的开发环境也是在容器中,项目已经给我们做好了封装,具体可以参考项目Readme的Development章节。

问几个问题,大家可以思考一下:

1. 开发是在容器中,可是容器里面连编辑器都没有,怎么开发?

答: 准确来说应该是在容器中调试,开发还是在本地开发,容器中看到的目录是我们本地目录mount进去的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# docker.Makefile文件
......
MOUNTS = -v "$(CURDIR)":/go/src/github.com/docker/cli
......
# start container in interactive mode for in-container development
.PHONY: dev
dev: build_docker_image
docker run -ti --security-opt=seccomp:unconfined $(ENVVARS) $(MOUNTS) \
-v /var/run/docker.sock:/var/run/docker.sock \
$(DEV_DOCKER_IMAGE_NAME) ash
shell: dev
......

从shell里面还是很容器看出来的,run的时候mount了$(CURDIR)

2. 容器里面是不是还装了一个docker啊,为什么我在里面敲命令有响应?

答:不是的,里面只有你编译生成的docker client程序,是没有docker deamon程序,至于为什么能响应,还是看上面的shell脚本,-v /var/run/docker.sock:/var/run/docker.sock这一行将docker.sock文件也mount了进去,docker.sockdocker deamon默认监听的Unix套接字(Unix domain socket),容器中的进程可以通过他与docker deamon进行通信。所以这里docker client通信的是你本机的docker daemon。

docker_deamon_socket

Note: 关于/var/run/docker.sock更多内容可以看这里

所以我们的开发流程是:本地改代码–>到container中编译–>运行docker client看效果

源码

是时候表演真正的技术了!

😆,开个玩笑,下面的分析如果有问题,还请大家不吝赐教!

我尽量把文件路径列出来,会贴一些代码,但主要还是路径,建议大家把代码clone下来照着看。

docker—client是基于cobra写的,建议大家先看一下cobra,至少写个hello world熟悉一下基本用法。

我们开始了!

入口文件在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
func main() {
// 日志输出采用了第三方库logrus
// Set terminal emulation based on platform as required.
stdin, stdout, stderr := term.StdStreams()
logrus.SetOutput(stderr)
dockerCli := command.NewDockerCli(stdin, stdout, stderr)
// root命令,子命令都在这里面,重点看
cmd := newDockerCommand(dockerCli)
// 命令执行
if err := cmd.Execute(); 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)
}
}

还是这个文件,newDockerCommand函数调用了commands.AddCommands(cmd, dockerCli)来添加命令。

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
func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
opts := cliflags.NewClientOptions()
var flags *pflag.FlagSet
cmd := &cobra.Command{
Use: "docker [OPTIONS] COMMAND [ARG...]",
Short: "A self-sufficient runtime for containers",
SilenceUsage: true,
SilenceErrors: true,
TraverseChildren: true,
Args: noArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if opts.Version {
showVersion()
return nil
}
return command.ShowHelp(dockerCli.Err())(cmd, args)
},
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
// daemon command is special, we redirect directly to another binary
if cmd.Name() == "daemon" {
return nil
}
// flags must be the top-level command flags, not cmd.Flags()
opts.Common.SetDefaultOptions(flags)
dockerPreRun(opts)
if err := dockerCli.Initialize(opts); err != nil {
return err
}
return isSupported(cmd, dockerCli)
},
}
cli.SetupRootCommand(cmd)
flags = cmd.Flags()
flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit")
flags.StringVar(&opts.ConfigDir, "config", cliconfig.Dir(), "Location of client config files")
opts.Common.InstallFlags(flags)
setFlagErrorFunc(dockerCli, cmd, flags, opts)
setHelpFunc(dockerCli, cmd, flags, opts)
cmd.SetOutput(dockerCli.Out())
cmd.AddCommand(newDaemonCommand())
# 添加子命令,cmd里现在是root命令
commands.AddCommands(cmd, dockerCli)
setValidateArgs(dockerCli, cmd, flags, opts)
return cmd
}

这行上面的cmd.AddCommand(newDaemonCommand())是为docker daemon命令进行的输出,newDaemonCommand定义在cmd/docker/daemon_none.go中,从他的RunE方法可以看出来,是输出了不能运行的提示,runtime.GOOS是输出当前系统的名称,比如mac是Darwin, ubuntu是linux,完整看来就是:

`docker daemon` is not supported on Darwin. Please run `dockerd` directly

让我们来到commands.AddCommands定义的地方,cli/command/commands/commands.go文件,可以看到cmd.AddCommand的调用,这里就是在添加我们看到的二级命令,也就是紧跟着docker后面的命令。

1
2
3
4
5
6
7
8
9
10
11
12
func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
cmd.AddCommand(
......
// container
container.NewContainerCommand(dockerCli),
container.NewRunCommand(dockerCli),
......
hide(container.NewPsCommand(dockerCli)),
hide(container.NewRenameCommand(dockerCli)),
......

我们从上看到下,整齐划一中发现最后好多命令被用hide包裹了。这些命令是一些旧命令,在新的版本中可以使用新的命令来代替,如果设置了环境变量DOCKER_HIDE_LEGACY_COMMANDS不为空,那么docker的提示将不会输出这些。

这里的命令太多了,我们通过docker ps命令来了解一下大致的执行流程。

docker ps命令也是旧版的命令,hide(container.NewPsCommand(dockerCli))就是在处理他。在新版本中对应的是docker container ls命令。实现代码在cli/command/container/list.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
// NewPsCommand creates a new cobra.Command for `docker ps`
func NewPsCommand(dockerCli *command.DockerCli) *cobra.Command {
options := psOptions{filter: opts.NewFilterOpt()}
cmd := &cobra.Command{
Use: "ps [OPTIONS]",
Short: "List containers",
Args: cli.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runPs(dockerCli, &options)
},
}
flags := cmd.Flags()
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display numeric IDs")
flags.BoolVarP(&options.size, "size", "s", false, "Display total file sizes")
flags.BoolVarP(&options.all, "all", "a", false, "Show all containers (default shows just running)")
flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output")
flags.BoolVarP(&options.nLatest, "latest", "l", false, "Show the latest created container (includes all states)")
flags.IntVarP(&options.last, "last", "n", -1, "Show n last created containers (includes all states)")
flags.StringVarP(&options.format, "format", "", "", "Pretty-print containers using a Go template")
flags.VarP(&options.filter, "filter", "f", "Filter output based on conditions provided")
return cmd
}
func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := *NewPsCommand(dockerCli)
cmd.Aliases = []string{"ps", "list"}
cmd.Use = "ls [OPTIONS]"
return &cmd
}

NewPsCommand对应的是docker ps, newListCommand对应的是docker container ls,可以看出来, newListCommand里的command就是NewPsCommand返回的,只是加了一下说明而已。

这里还漏了一点,docker container ls是3个命令,我们知道docker是root命令,按照cobra的写法,container和ls应该是分开的。他们的关系是这样的,在cli/command/commands/commands.go文件中container.NewContainerCommand(dockerCli)加入了container命令,NewContainerCommand的实现在cli/command/container/cmd.go中,在NewContainerCommand函数中通过newListCommand(dockerCli)加入了ls命令,可以看到还添加了很多别的方法,那些都是container支持的子命令。这样就回到了我们上面提到的内容了,没有魔法

我们继续看NewPsCommand:

里面定义了docker ps(下面所有的内容对docker container ls都适用)支持的一些选项(flag),选项被放在了psOptions这个结构体中,最终传给runPs函数.

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
func runPs(dockerCli *command.DockerCli, options *psOptions) error {
ctx := context.Background()
listOptions, err := buildContainerListOptions(options)
if err != nil {
return err
}
containers, err := dockerCli.Client().ContainerList(ctx, *listOptions)
if err != nil {
return err
}
format := options.format
if len(format) == 0 {
if len(dockerCli.ConfigFile().PsFormat) > 0 && !options.quiet {
format = dockerCli.ConfigFile().PsFormat
} else {
format = formatter.TableFormatKey
}
}
containerCtx := formatter.Context{
Output: dockerCli.Out(),
Format: formatter.NewContainerFormat(format, options.quiet, listOptions.Size),
Trunc: !options.noTrunc,
}
return formatter.ContainerWrite(containerCtx, containers)
}

listOptions, err := buildContainerListOptions(options)对选项进行了处理,重点在dockerCli.Client().ContainerList(ctx, *listOptions)ContainerList是一个interface。

实现在vendor/github.com/docker/docker/client/container_list.go(vendor在当前目录下,我是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
func (cli *Client) ContainerList(ctx context.Context, options types.ContainerListOptions) ([]types.Container, error) {
query := url.Values{}
if options.All {
query.Set("all", "1")
}
if options.Limit != -1 {
query.Set("limit", strconv.Itoa(options.Limit))
}
if options.Since != "" {
query.Set("since", options.Since)
}
if options.Before != "" {
query.Set("before", options.Before)
}
if options.Size {
query.Set("size", "1")
}
if options.Filters.Len() > 0 {
filterJSON, err := filters.ToParamWithVersion(cli.version, options.Filters)
if err != nil {
return nil, err
}
query.Set("filters", filterJSON)
}
resp, err := cli.get(ctx, "/containers/json", query, nil)
if err != nil {
return nil, err
}
var containers []types.Container
err = json.NewDecoder(resp.body).Decode(&containers)
ensureReaderClosed(resp)
return containers, err
}

前面是在拼请求的参数,resp, err := cli.get(ctx, "/containers/json", query, nil)这里发起了请求,返回了container的列表。我们可以通过curl或nc来获得原始的数据:

1
2
3
4
5
➜ curl -XGET --unix-socket /var/run/docker.sock http://localhost/containers/json
或者
➜ echo 'GET /containers/json HTTP/1.0\r\n\r\n' | nc -U /var/run/docker.sock
[{"Id":"c64ea306f74980555521ad2fcbb4c54e1b05e7ca0d597838557d4d613ed12039","Names":["/brave_swirles"],"Image":"docker-cli-dev","ImageID":"sha256:b123a157eaf7e4d468647b42be4c8e339e9e4b9e84058a56575dbfa7249161c9","Command":"ash","Created":1505218585,"Ports":[],"Labels":{},"State":"running","Status":"Up 25 hours","HostConfig":{"NetworkMode":"default"},"NetworkSettings":{"Networks":{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"NetworkID":"4b149c263b9a12caef2b16482db33fded74eac5396a57da1724428948464e00f","EndpointID":"6a3fb06309f2142b23db84303cbdb5d55b3a41e91cf2468649bd5f2adee11c66","Gateway":"172.17.0.1","IPAddress":"172.17.0.2","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:11:00:02","DriverOpts":null}}},"Mounts":[{"Type":"bind","Source":"/Users/youwangqiu/go_code/src/github.com/docker/cli","Destination":"/go/src/github.com/docker/cli","Mode":"","RW":true,"Propagation":"rprivate"},{"Type":"bind","Source":"/var/run/docker.sock","Destination":"/var/run/docker.sock","Mode":"","RW":true,"Propagation":"rprivate"}]}]

可以看到数据是以json格式返回的,是一个列表。

回到cli/command/container/list.go,接收到返回值之后,这时候还是上面看到的json,下面进行了格式化输出,大家应该都有做打印9*9乘法表的经历,这里的格式化是同样的道理,不明白的同学可以继续往挖,我偷个懒,就不继续了。

至此docker ps的流程就走完了,大家可以对源码做一些修改,然后进到容器中执行

1
make binary

就可以生产新的docker client,执行一下就能得到想要的输出啦。

谢谢大家,欢迎大家指正🙂