在上文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的启动与命令执行。 |