首页 > 代码库 > Docker源码分析之——Docker Client的启动与命令执行

Docker源码分析之——Docker Client的启动与命令执行

        在上文Docker源码分析之——Docker Daemon的启动 中,介绍了Docker Daemon进程的启动。Docker Daemon可以认为是一个Docker作为Server的运行载体,而真正发送关于docker container操作的请求的载体,在于Docker Client。本文从Docker源码的角度,分析Docker Client启动与执行请求的过程。


        Docker Client启动的流程与Docker Daemon启动的过程相仿。首先执行reexec.Init();随后解析flag参数;由于没有指定-v,-D参数,因此*flVersion与*flDebug均为false,不执行相应的代码块;接着程序执行flHosts信息的获取与验证;又由于没有指定-d参数,故*flDeamon为false,不会执行mainDaemon()方法所在的代码块;最后,接着执行的都是Docker Client启动以及执行的工作。


        首先,验证flHosts参数中是带有多个Host的信息,如tcp://host:port,unix://path_to_socket或者fd://socketfd,当flHosts的长度大于1,即Host信息不止一个时,通过log的形式通知用户最好指定一个Host。然后,默认将flHosts信息中的第一个对象作为Host,进行协议与host地址的解析,代码如下:

if len(flHosts) > 1 {
	log.Fatal("Please specify only one -H")
}
protoAddrParts := strings.SplitN(flHosts[0], "://", 2)

        接着,创建一个Docker Client的cli以及与安全传输层协议TLS相关的对象tlsConfig。安全传输层协议(TLS)用于两个通信应用程序之间保密性与数据完整性,该协议有两层组成:TLS记录协议和TLS握手协议。代码如下:

var (
	cli       *client.DockerCli
	tlsConfig tls.Config
)
tlsConfig.InsecureSkipVerify = true

        随后,如果执行docker命令的时候指定了-tlsverify参数,则*flTlsVerify为true,说明Docker  Client需要加载一个受信的ca,用以验证 Docker Server,执行一下代码块:

// If we should verify the server, we need to load a trusted ca
if *flTlsVerify {
	*flTls = true
	certPool := x509.NewCertPool()
	file, err := ioutil.ReadFile(*flCa)
	if err != nil {
		log.Fatalf("Couldn't read ca cert %s: %s", *flCa, err)
	}
	certPool.AppendCertsFromPEM(file)
	tlsConfig.RootCAs = certPool
	tlsConfig.InsecureSkipVerify = false
}

        可以看到,最要的就是*flCa参数,而该参数在./docker/flag.go中的init()方法中初始化时被赋值,如下:

flCa = flag.String([]string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Trust only remotes providing a certificate signed by the CA given here")

var (
	dockerCertPath = os.Getenv("DOCKER_CERT_PATH")
)

const (
	defaultCaFile   = "ca.pem"
	defaultKeyFile  = "key.pem"
	defaultCertFile = "cert.pem"
)

        因此,可以通过以上信息,获取*flCa,并读取相应的ca.pem文件,最终添加至tlsConfig对象的属性RootCAs和InsecureSkipVerify中。


        验证完server,添加了受信ca后,Docker Client需要配置认证信息,以便之后的发送。主要还是给tlsConfig对象中添加属性Certificates,代码如下:

// If tls is enabled, try to load and send client certificates
if *flTls || *flTlsVerify {
	_, errCert := os.Stat(*flCert)
	_, errKey := os.Stat(*flKey)
	if errCert == nil && errKey == nil {
		*flTls = true
		cert, err := tls.LoadX509KeyPair(*flCert, *flKey)
		if err != nil {
			log.Fatalf("Couldn't load X509 key pair: %s. Key encrypted?", err)
		}
		tlsConfig.Certificates = []tls.Certificate{cert}
	}
}

        以下真正给Docker Client的cli对象初始化,主要的区别在于是否需要使用安全传输层协议,即只要*flTls和*flTlsVerify中有一个为true,则说明需要使用安全传输层协议;反之,不使用安全传输层协议。

if *flTls || *flTlsVerify {
	cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
} else {
	cli = client.NewDockerCli(os.Stdin, os.Stdout, os.Stderr, protoAddrParts[0], protoAddrParts[1], nil)
}

        client.NewDockerCli()的实现位于./api/client/cli.go,代码如下:

func NewDockerCli(in io.ReadCloser, out, err io.Writer, proto, addr string, tlsConfig *tls.Config) *DockerCli {
	var (
		isTerminal = false
		terminalFd uintptr
		scheme     = "http"
	)

	if tlsConfig != nil {
		scheme = "https"
	}

	if in != nil {
		if file, ok := out.(*os.File); ok {
			terminalFd = file.Fd()
			isTerminal = term.IsTerminal(terminalFd)
		}
	}

	if err == nil {
		err = out
	}
	return &DockerCli{
		proto:      proto,
		addr:       addr,
		in:         in,
		out:        out,
		err:        err,
		isTerminal: isTerminal,
		terminalFd: terminalFd,
		tlsConfig:  tlsConfig,
		scheme:     scheme,
	}
}

        总体而言,创建DockerCli对象较为简单,较为重要的DockerCli的属性有proto:传输协议;addr:host的目标地址,tlsConfig:安全传输层协议的配置。若tlsConfig为有内容,则说明需要使用安全传输层协议,DockerCli对象的scheme设置为“https”,另外还有关于输入,输出以及错误显示的配置,最终返回该对象。


        在./docker/docker.go中,创建完给cli对象初始化之后,执行的就是用户所指定的命令了,代码如下:

if err := cli.Cmd(flag.Args()...); err != nil {
	if sterr, ok := err.(*utils.StatusError); ok {
		if sterr.Status != "" {
			log.Println(sterr.Status)
		}
		os.Exit(sterr.StatusCode)
	}
	log.Fatal(err)
}

        在该过程中,真正的执行载体为cli.Cmd(flag.Args()...),该代码解析并执行真正如“docker pull xxx”, “docker search xxx”等的指令。


        进入./api/client/cli.go中的Cmd方法,代码如下:

// Cmd executes the specified command
func (cli *DockerCli) Cmd(args ...string) error {
	if len(args) > 0 {
		method, exists := cli.getMethod(args[0])
		if !exists {
			fmt.Println("Error: Command not found:", args[0])
			return cli.CmdHelp(args[1:]...)
		}
		return method(args[1:]...)
	}
	return cli.CmdHelp(args...)
}

        首先Cmd方法通过第一个参数来执行getMethod方法,若该method没有找到的话,则调用CmdHelp方法,若method找到,则执行method方法,传入参数为第一个开始往后所有的参数,代码如下:

func (cli *DockerCli) getMethod(name string) (func(...string) error, bool) {
	if len(name) == 0 {
		return nil, false
	}
	methodName := "Cmd" + strings.ToUpper(name[:1]) + strings.ToLower(name[1:])
	method := reflect.ValueOf(cli).MethodByName(methodName)
	if !method.IsValid() {
		return nil, false
	}
	return method.Interface().(func(...string) error), true
}

        以下以命令“docker pull xxx”为例,分析Docker Client的Cmd的运行。首先,载Cmd方法中传入的参数为“pull”,“xxx”;则getMethod方法传入参数只有一个,为“pull”;getMethod方法返回CmdPull;则在Cmd方法中执行CmdPull(args[1:]...)。而CmdPull方法的实现位于./api/client/command.go中的CmdPull。


        在具体的CmdXxx方法中,一般都需要通过cli于Docker Server进行通信,如CmdPull方法实现中有cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out, map[string][]string{ "X-Registry-Auth": registryAuthHeader,}) ,而cli的stream方法实现,以及类似的call方法实现,都位于./api/client/utils.go中。utils.go中主要实现了cli如何一个HTTPClient的形式给server发送请求,以及如果需要的话,如何在整个发送过程使用安全传输协议。


        当cli请求发送完毕,且接收到相应的相应之后,则对于Docker Client而言,执行一次的生命周期就结束了。当你需要另外再执行一个命令时,则需要以上的流程再走一遍。


        以上便是从Docker源码的角度,分析Docker Client的启动与命令执行。


转载请注明出处。

本文更多出于我本人的理解,肯定在一些地方存在不足和错误。希望本文能够对接触Docker的人有些帮助,如果你对这方面感兴趣,并有更好的想法和建议,也请联系我。

我的邮箱:shlallen@zju.edu.cn
新浪微博:@莲子弗如清

Docker源码分析之——Docker Client的启动与命令执行