docker源码学习之 docker run 子命令

说明: 使用的代码是 https://github.com/golang108/docker-cli.git (tag: v24.0.6)

1. 添加容器相关的几个子命令

其中就有我们这里说到 run 子命令。

容器相关的 子命令 在这里 被加入到 corba 中去

cli/command/commands/commands.go 文件

1
2
3
4
5
6
7
8
9
10
11

func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
cmd.AddCommand(
container.NewRunCommand(dockerCli), 这里加入 container 相关的 几个子命令,例如 docker run、docker exec, docker ps 这3个子命令。
container.NewExecCommand(dockerCli),
container.NewPsCommand(dockerCli),
......
......
......
)
}

整个命令的一个调用执行 流程如下:

1
2
3
4
5
6
7
8

main.runDocker里的 cmd.Execute() ->
corba中的 c.ExecuteC() ->
corba中的 cmd.execute(flags) -> 1068行
corba中的 err = c.ParseFlags(a) 861行,在这里解析了flags,然后顺便 给 copts 中字段属性赋上真正的值了!!!来自github.com/spf13/cobra/command.go文件。
corba中的 c.RunE(c, argWoFlags); ->
container/run.go中的 runRun(dockerCli, cmd.Flags(), &options, copts)

2. NewRunCommand() 函数

1
2
3
4
func NewRunCommand(dockerCli command.Cli) *cobra.Command {

}

1️⃣这个函数 主要作用 就是 初始化 cobra.Command{} 结构体 指针。然后返回这个指针。

2️⃣然后 设置 flags的解析。

2.1 cmd := &cobra.Command{} 结构体的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func NewRunCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "run [OPTIONS] IMAGE [COMMAND] [ARG...]",
Short: "Create and run a new container from an image",
Args: cli.RequiresMinArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
copts.Image = args[0]
if len(args) > 1 {
copts.Args = args[1:]
}
return runRun(dockerCli, cmd.Flags(), &options, copts)
},
ValidArgsFunction: completion.ImageNames(dockerCli),
Annotations: map[string]string{
"category-top": "1",
"aliases": "docker container run, docker run",
},
}

return cmd

}

所以 看 docker run 命令的 主要 功能呢 就是看 这个 runRun(dockerCli, cmd.Flags(), &options, copts) 函数

2.2 添加 flags

1️⃣ 单独在 NewRunCommand() 函数内添加的。对应到 ropts runOptions。

经过 flags.BoolVarP,flags.StringVar 等函数的执行,结构体里面的属性字段, 并且都用指针 和 flags 绑定 上了。

2️⃣ 另外一类是在 copts = addFlags(flags) 函数内添加的。对应到 copts *containerOptions。

经过 addFlags(flags) 函数,这个 copts 结构体实例算是创建好了,里面的属性都是零值了,并且都用指针 和 flags 绑定上了。

等 cobra 解析 flags 的时候 就会把 命令上的选项,参数 赋值 到 copts 实例里面的字段上了。

3️⃣ 其他 flags

1
2
command.AddPlatformFlag(flags, &options.platform)
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())

4️⃣注册 RegisterFlagCompletionFunc 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmd.RegisterFlagCompletionFunc(
"env",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return os.Environ(), cobra.ShellCompDirectiveNoFileComp
},
)
cmd.RegisterFlagCompletionFunc(
"env-file",
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveDefault
},
)
cmd.RegisterFlagCompletionFunc(
"network",
completion.NetworkNames(dockerCli),
)

3. runRun(dockerCli, flags, ropts, copts)函数

执行到这个 runRun() 函数的时候 copts 里面的属性值有些都确定了。

3.1 ropts *runOptions 参数介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
type createOptions struct {
name string
platform string
untrusted bool
pull string // always, missing, never
quiet bool
}
type runOptions struct {
createOptions 这里内嵌了一个 结构体,等于2个结构体组合到一起了
detach bool
sigProxy bool
detachKeys string
}
ropts 这个参数 是 `var options runOptions` , 在 func NewRunCommand() 函数开头
定义的一个, 是一个结构体类型。 里面字段的属性值 也是和 flags绑定的。
这个和flags的绑定没有单独弄个函数出来,是直接在 NewRunCommand() 函数 里面写了。
例如: -d, --name,--quiet,--pull 等选项。
这些选项没有存放在 Config/HostConfig 结构体里面(containerCfg结构体里面包含着2个属性字段)。
这个称之为 run options。

3.2 copts *containerOptions 参数介绍

这个结构体里面 的属性很多,下面简单介绍几个,

1
2
3
4
5
6
7
8
9
10
// containerOptions is a data object with all the options for creating a container
type containerOptions struct {
volumes opts.ListOpts 挂载的目录 就是 -v 对应的参数
tty bool 这个是true ,指定了 -t 参数
user string 这个是 1000:1000 --user选项知道的。
workingDir string
autoRemove bool 这是 true,通过 --rm 参数指定了
Image string 指定的镜像 harbor.magesfc.com/library/centos:7
Args []string 最后的剩下的参数
}

其实这结构体就是 run 子命令 后面跟的那些参数,选项等。

copts 这个参数 是 `var copts *containerOptions`, 同上一样的。也是一个结构体。 
里面字段的属性值 也是和 flags绑定的。经过 addFlags(flags) 函数进行绑定了。
这个称之为 container Options。

3.3 containerCfg, err := parse(flags, copts, serverOS) 函数

这个函数 用来 生成 containerCfg 结构体实例。

1️⃣ serverOS 参数就是 “linux” 或 “windows”

2️⃣ containerCfg 是返回值,类型是 containerConfig指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
type containerConfig struct {
Config *container.Config
HostConfig *container.HostConfig
NetworkingConfig *networktypes.NetworkingConfig
}

这里在函数的最后 返回 containerConfig 结构体的一个指针。
return &containerConfig{
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
}

4. runContainer(dockerCli, ropts, copts, containerCfg)

ropts, copts 2个参数的介绍 参考 3.1和3.2

containerCfg 这个参数是通过 containerCfg, err := parse(flags, copts, OSType) 函数得到的。

5. createContainer(ctx, dockerCli, containerCfg, &opts.createOptions)

1
2
3
config := containerCfg.Config
hostConfig := containerCfg.HostConfig
networkingConfig := containerCfg.NetworkingConfig

这里面会 定义了一个 pullAndTagImage 变量。这个变量是个函数,里面 有个 pullImage(ctx, dockerCli, config.Image, opts) 的调用。

流程是 先执行 dockerCli.Client().ContainerCreate() 尝试创建容器,如果没有创建成功,才会 执行那个拉取镜像的动作。

如果本地镜像已经存在,就会尝试创建容器的。

如果本地镜像不存在,走 pullImage() -> dockerCli.Client().ImageCreate() 拉取镜像这个流程

1
2
3
4
5
6
7
8
9
10
11
12
13
第一次尝试, 如果没有成功,这里的 response  会是个空字符串,如果成功了,会是一个容器的ID。
response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.name)
如果 err 有错误,会在 if 里面进行 拉取镜像的尝试的, 这里的 err是 No such image: harbor.magesfc.com/library/xxx:7。 因为我们故意写错镜像名称了。
if err != nil {

if err := pullAndTagImage(); err != nil {
return nil, err
}

这里是第二次 尝试 创建容器了
response, retryErr = dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, platform, opts.name)

}

dockerCli.Client().ImageCreate() 里面是 调用 “/images/create” 接口发送post请求。

6. dockerCli.Client().ContainerCreate()

到这一步 就会 发送 http 相关请求 到 dockerd 服务上了。

1
2
3
4
5
containerCfg{
Config
HostConfig -> Binds -v 参数 挂载目录
NetworkingConfig
}

dockerCli.Client().ContainerCreate() 里面 调用 “/containers/create” 接口发送post请求。带的body就是 containerCfg 中的3个属性字段。

1
2
3
4
5
6
7
8
9
body := configWrapper{
Config: config,
HostConfig: hostConfig,
NetworkingConfig: networkingConfig,
}


serverResp, err := cli.post(ctx, "/containers/create", query, body, nil)

1
2
3
4
5
6
7
服务器端注册路由:
router.NewPostRoute("/containers/create", r.postContainersCreate),

路由处理函数
func (s *containerRouter) postContainersCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {


body := configWrapper{} -> 会转为json字符串发送请求带上。 然后在 服务端接收的时候 会再次 把 json字符串转换为 对应的结构体的。

1
config, hostConfig, networkingConfig, err := s.decoder.DecodeConfig(r.Body)

一次执行的body的内容:

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
{
"Hostname":"",
"Domainname":"",
"User":"1000:1000",
"AttachStdin":true,
"AttachStdout":true,
"AttachStderr":true,
"Tty":true,
"OpenStdin":true,
"StdinOnce":true,
"Env":null,
"Cmd":[
"id"
],
"Image":"harbor.magesfc.com/library/centos:7",
"Volumes":{

},
"WorkingDir":"",
"Entrypoint":null,
"OnBuild":null,
"Labels":{

},
"HostConfig":{
"Binds":[
"/home:/home",
"/data:/data",
"/tmp:/tmp",
"/etc/passwd:/etc/passwd:ro",
"/etc/group:/etc/group:ro"
],
"ContainerIDFile":"",
"LogConfig":{
"Type":"",
"Config":{

}
},
"NetworkMode":"default",
"PortBindings":{

},
"RestartPolicy":{
"Name":"no",
"MaximumRetryCount":0
},
"AutoRemove":true,
"VolumeDriver":"",
"VolumesFrom":null,
"ConsoleSize":[
30,
122
],
"CapAdd":null,
"CapDrop":null,
"CgroupnsMode":"",
"Dns":[

],
"DnsOptions":[

],
"DnsSearch":[

],
"ExtraHosts":null,
"GroupAdd":null,
"IpcMode":"",
"Cgroup":"",
"Links":null,
"OomScoreAdj":0,
"PidMode":"",
"Privileged":false,
"PublishAllPorts":false,
"ReadonlyRootfs":false,
"SecurityOpt":null,
"UTSMode":"",
"UsernsMode":"",
"ShmSize":0,
"Isolation":"",
"CpuShares":0,
"Memory":0,
"NanoCpus":0,
"CgroupParent":"",
"BlkioWeight":0,
"BlkioWeightDevice":[

],
"BlkioDeviceReadBps":[

],
"BlkioDeviceWriteBps":[

],
"BlkioDeviceReadIOps":[

],
"BlkioDeviceWriteIOps":[

],
"CpuPeriod":0,
"CpuQuota":0,
"CpuRealtimePeriod":0,
"CpuRealtimeRuntime":0,
"CpusetCpus":"",
"CpusetMems":"",
"Devices":[

],
"DeviceCgroupRules":null,
"DeviceRequests":null,
"MemoryReservation":0,
"MemorySwap":0,
"MemorySwappiness":-1,
"OomKillDisable":false,
"PidsLimit":0,
"Ulimits":null,
"CpuCount":0,
"CpuPercent":0,
"IOMaximumIOps":0,
"IOMaximumBandwidth":0,
"MaskedPaths":null,
"ReadonlyPaths":null
},
"NetworkingConfig":{
"EndpointsConfig":{

}
}
}

容器名称是 带到 query 参数中了, 这里我们传入的是 –name my