docker命令行参数解析流程

通过 docker -v 查看版本,可以得到 对应 仓库的 revision。然后克隆这个仓库,检出对应的源代码。

1
2
$ docker -v    
Docker version 24.0.2, build cb74dfc

克隆这个仓库,检出对应的源代码

1
2
3
4
5
git clone https://github.com/golang108/docker-cli.git


git checkout cb74dfc # 这个就对应 Docker version 24.0.2, build cb74dfc

1. 入口 main 函数

来自文件 cmd/docker/docker.go

1
2
3
4
5
6
7
8
9
10
11
12

func main() {
// 1. 主要就是 实例化 一个 dockerCli 这个东西,
// dockerCli 其实是 type DockerCli struct 这样的一个结构体
dockerCli, err := command.NewDockerCli()

// 2. 然后就是 调用 runDocker 函数,传入刚刚实例化出来的 dockerCli 对象。
if err := runDocker(dockerCli); err != nil {

}


2. command.NewDockerCli() 函数

2.1 DockerCli 结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type DockerCli struct {
configFile *configfile.ConfigFile
options *cliflags.ClientOptions
in *streams.In
out *streams.Out
err io.Writer
client client.APIClient
serverInfo ServerInfo
contentTrust bool
contextStore store.Store
currentContext string
init sync.Once
initErr error
dockerEndpoint docker.Endpoint
contextStoreConfig store.Config
initTimeout time.Duration
}

这个DockerCli结构体 实现了 type Cli interface 接口的方法,type Cli interface 接口 里面包含了 type Streams interface 接口。

这个DockerCli结构体 同时也实现了 type Streams interface 接口的方法

2.2 两个 接口 Streams和Cli

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
// Streams is an interface which exposes the standard input and output streams
type Streams interface {
In() *streams.In
Out() *streams.Out
Err() io.Writer
}

// Cli represents the docker command line client.
type Cli interface {
Client() client.APIClient
Streams
SetIn(in *streams.In)
Apply(ops ...DockerCliOption) error
ConfigFile() *configfile.ConfigFile
ServerInfo() ServerInfo
NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
DefaultVersion() string
CurrentVersion() string
ManifestStore() manifeststore.Store
RegistryClient(bool) registryclient.RegistryClient
ContentTrustEnabled() bool
BuildKitEnabled() (bool, error)
ContextStore() store.Store
CurrentContext() string
DockerEndpoint() docker.Endpoint
}

2.3 函数 NewDockerCli(ops …DockerCliOption) (*DockerCli, error)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

func NewDockerCli(ops ...DockerCliOption) (*DockerCli, error) {
defaultOps := []DockerCliOption{
WithContentTrustFromEnv(),
WithDefaultContextStoreConfig(),
WithStandardStreams(),
}
ops = append(defaultOps, ops...)

cli := &DockerCli{}
if err := cli.Apply(ops...); err != nil {
return nil, err
}
return cli, nil
}

实例化 DockerCli 的时候 可以 传入 DockerCliOption 类型的参数,但是我们这里没有传参!!!

2.4 DockerCliOption 参数

这个参数 会 被 Apply 到 DockerCli 实例对象里的:cli.Apply(ops...)

一个一个的ops是一个一个的函数: type DockerCliOption func(cli *DockerCli) error, 然后就会想这样去调用 op(cli)

其实真正要做的 就是 修改,或者初始化 cli.xxxx 这样的值,说白了就是要初始化 cli 实例里面属性的值的。

例如下面这个函数 就可以 作为 DockerCliOption 类型,传入 。

1
2
3
4
func(cli *DockerCli) error {
cli.client = c
return nil
}

golang 中就有很多 这样的设计, 一个 Apply() 函数, 然后 一堆 WithXXXX() 这样的函数, 参考 cli/command/cli.go 和 cli/command/cli_options.go 文件。

3. runDocker(dockerCli) 函数

3.1 newDockerCommand(dockerCli) 函数

1
tcmd := newDockerCommand(dockerCli)

3.1.1 初始化 一个 &cobra.Command{} 结构体指针

1
2
3
4
5
6
cmd := &cobra.Command{
Use: "docker [OPTIONS] COMMAND [ARG...]",
Short: "A self-sufficient runtime for containers",
以下代码省略
。。。。。。
}

3.1.2 cli.SetupRootCommand(cmd) 函数, docker.go中的57行

1
2
3
opts, helpCmd = cli.SetupRootCommand(cmd)


根命令: docker [OPTIONS] COMMAND [ARG…]

1
2
3
4
5
6
// SetupRootCommand sets default usage, help, and error handling for the
// root command.
func SetupRootCommand(rootCmd *cobra.Command) (opts *cliflags.ClientOptions, helpCmd *cobra.Command) {
rootCmd.SetVersionTemplate("Docker version {{.Version}}\n")
return setupCommonRootCommand(rootCmd)
}

3.1.3 commands.AddCommands(cmd, dockerCli), docker.go中的66行

1
2
commands.AddCommands(cmd, dockerCli)

1
2
3
4
5
6
7
8
9
10
11
12
13
14

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),
......
......
......
)
}
这个里面会调用 cobra.Command 中的 AddCommand() 函数 ,把子命令 都给加入进去。
func (c *Command) AddCommand(cmds ...*Command) {

3.1.4 返回 NewTopLevelCommand() -> TopLevelCommand{} 结构体的一个指针, docker.go中的72行

1
2
3
4
5
6
7
8
9
func NewTopLevelCommand(cmd *cobra.Command, dockerCli *command.DockerCli, opts *cliflags.ClientOptions, flags *pflag.FlagSet) *TopLevelCommand {
return &TopLevelCommand{
cmd: cmd, // --》 *cobra.Command 类型
dockerCli: dockerCli, // --》 *command.DockerCli 类型
opts: opts, // --》 *cliflags.ClientOptions 类型
flags: flags, // --》 *pflag.FlagSet 类型
args: os.Args[1:], // 命令 其他参数,本次执行的 是 docker run --name my --user 1000:1000 xxx 这样的命令。这里的args 就是 除了第一个后面的所有的。
}
}

3.2 HandleGlobalFlags() 函数

3.3 tcmd.Initialize()

3.4 processAliases(dockerCli, cmd, args, os.Args)

3.5 pluginmanager 相关处理

1
2
3
4
5
6
7
8
9
10
11
12
13
if len(args) > 0 {
ccmd, _, err := cmd.Find(args)
if err != nil || pluginmanager.IsPluginCommand(ccmd) {
err := tryPluginRun(dockerCli, cmd, args[0], envs)
if !pluginmanager.IsNotFound(err) {
return err
}
// For plugin not found we fall through to
// cmd.Execute() which deals with reporting
// "command not found" in a consistent way.
}
}

一个命令是 docker-xxxx 这样命名的,并且放入到环境变量 PATH 中去了 就是一个plugin相关的命令了。

类似这样做的 有 git-xxx 这样的命令,k8s 中也有。。。

这就是 命令埋得扩展点!!!!可以方便的支持扩展更多功能的子命令!!!

3.5.1 cmd.Find(args) 函数

这个 Find() 函数 也是 corba 里面提供的。

3.5.2 IsPluginCommand(ccmd) 函数

3.5.3 tryPluginRun(dockerCli, cmd, args[0], envs) 函数

3.6 重头戏开始,执行 cmd.Execute() 函数

调用 corba 中的 Execute() 函数了,这个会自动 调用 子命令中的 Execute() 函数的

例如: 执行了 docker run --name xxx --user 1000:1000 image:v1.0 这样的命令 下面就会调用到:
run 这个子命令, 这个子命令也是 在这里 cli/command/commands/commands.go 文件 container.NewRunCommand(dockerCli) , 加入 corba 中的。

这些命令行的执行 流程,命令行参数的解析 都是这个第三方库 corba 做的。

1
return cmd.Execute()

详细了解 corba 可以 参考 github.com/spf13/cobra