《容器技術係列》一2.2 創建Docker Client
本節書摘來異步社區《容器技術係列》一書中的第2章 ,第2.2節,孫宏亮 著, 更多章節內容可以訪問雲棲社區“異步社區”公眾號查看。
2.2 創建Docker Client
對於Docker這樣一個Client/Server的架構,客戶端的存在意味著Docker相應任務的發起。用戶首先需要創建一個DockerClient,隨後將特定的請求類型與參數傳遞至Docker Client,最終由Docker Client轉義成Docker Server能識別的形式,並發送至Docker Server。
Docker Client的創建實質上是Docker用戶通過二進製可執行文件docker,創建與Docker Server建立聯係的客戶端。以下分3個小節分別闡述Docker Client的創建流程。
Docker Client完整的運行流程如圖2-1所示。
通過學習圖2-1,我們可以更為清晰地了解Docker Client創建及執行請求的過程。其中涉及諸多Docker源碼層次中的專有名詞,本章後續會一一解釋與分析。
2.2.1 Docker命令的flag參數解析
眾所周知,在Docker的具體實現中,Docker Server與Docker Client均由可執行文件docker來完成創建並啟動。那麼,了解docker可執行文件通過何種方式來區分到底是Docker Server還是Docker Client,就顯得尤為重要。
首先通過docker命令舉例說明其中的區別。Docker Server的啟動,命令為docker -d或docker --daemon=true;而Docker Client的啟動則體現為docker --daemon=false ps、docker pull NAME等。
其實,對於Docker請求中的參數,我們可以將其分為兩類:第一類為命令行參數,即docker程序運行時所需提供的參數,如: -D、--daemon=true、--daemon=false等;第二類為docker發送給Docker Server的實際請求參數,如:ps、pull NAME等。
對於第一類,我們習慣將其稱為flag參數,在Go語言的標準庫中,專門為該類參數提供了一個flag包,方便進行命令行參數的解析。
清楚docker二進製文件的使用以及基本的命令行flag參數之後,我們可以進入實現Docker Client創建的源碼中,位於./docker/docker/docker.go。這個go文件包含了整個Docker的main函數,也就是整個Docker(不論Docker Daemon還是Docker Client)的運行入口。部分main函數代碼如下:
func main() {
if reexec.Init() {
return
}
flag.Parse()
// FIXME: validate daemon flags here
...
}
以上源碼實現中,首先判斷reexec.Init()方法的返回值,若為真,則直接退出運行,否則將繼續執行。reexec.Init()函數的定義位於./docker/reexec/reexec.go,可以發現由於在docker運行之前沒有任何Initializer注冊,故該代碼段執行的返回值為假。reexec存在的作用是:協調execdriver與容器創建時dockerinit這兩者的關係。第13章在分析dockerinit的啟動時,將詳細講解reexec的作用。
判斷reexec.Init()之後,Docker的main函數通過調用flag.Parse()函數,解析命令行中的flag參數。如果熟悉Go語言中的flag參數,一定知道解析flag參數的值之前,程序必須先定義相應的flag參數。進一步查看Docker的源碼,我們可以發現Docker在./docker/docker/flag.go中定義了多個flag參數,並通過init函數進行部分flag參數的初始化。代碼如下:
var (
flVersion = flag.Bool([]string{"v", "-version"}, false, "Print version information and quit")
flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode")
flDebug = flag.Bool([]string{"D", "-debug"}, false, "Enable debug mode")
flSocketGroup = flag.String([]string{"G", "-group"}, "docker", "Group to assign the unix socket specified by -H when running in daemon modeuse '' (the empty string) to disable setting of a group")
flEnableCors = flag.Bool([]string{"#api-enable-cors", "-api-enable-cors"}, false, "Enable CORS headers in the remote API")
flTls = flag.Bool([]string{"-tls"}, false, "Use TLS; implied by tls-verify flags")
flTlsVerify = flag.Bool([]string{"-tlsverify"}, false, "Use TLS and verify the remote (daemon: verify client, client: verify daemon)")
// these are initialized in init() below since their default values depend on dockerCertPath which isn't fully initialized until init() runs
flCa *string
flCert *string
flKey *string
flHosts []string
)
func init() {
flCa = flag.String([]string{"-tlscacert"}, filepath.Join(dockerCertPath, defaultCaFile), "Trust only remotes providing a certificate signed by the CA given here")
flCert = flag.String([]string{"-tlscert"}, filepath.Join(dockerCertPath, defaultCertFile), "Path to TLS certificate file")
flKey = flag.String([]string{"-tlskey"}, filepath.Join(dockerCertPath, defaultKeyFile), "Path to TLS key file")
opts.HostListVar(&flHosts, []string{"H", "-host"}, "The socket(s) to bind to in daemon mode\nspecified using one or more tcp://host:port, unix:///path/to/socket, fd://* or fd://socketfd.")
}
以上源碼展示了Docker如何定義flag參數,以及在init函數中實現部分flag參數的初始化。Docker的main函數執行前,這些變量創建以及初始化工作已經全部完成。這裏涉及了Go語言的一個特性,即init函數的執行。Go語言中引入其他包(import package)、變量的定義、init函數以及main函數這四者的執行順序如圖2-2所示。
關於Golang中的init函數,深入分析可以得出以下特性:
init函數用於程序執行前包的初始化工作,比如初始化變量等;
每個包可以有多個init函數;
包的每一個源文件也可以有多個init函數;
同一個包內的init函數的執行順序沒有明確的定義;
不同包的init函數按照包導入的依賴關係決定初始化的順序;
init函數不能被調用,而是在main函數調用前自動被調用。
清楚Go語言一些基本的特性之後,回到Docker中來。Docker的main函數執行之前,Docker已經定義了諸多flag參數,並對很多flag參數進行初始化。定義並初始化的命令行flag參數有:flVersion、flDaemon、flDebug、flSocketGroup、flEnableCors、flTls、flTlsVerify、flCa、flCert、flKey、flHosts等。
以下具體分析flDaemon:
- 定義:flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode");
- flDaemon的類型為Bool類型;
- flDaemon名稱為"d"或者"-daemon",該名稱會出現在docker命令中,如docker –d;
- flDaemon的默認值為false;
- flDaemon的用途信息為"Enable daemon mode";
- 訪問flDaemon的值時,使用指針*flDaemon解引用訪問。
- 在解析命令行flag參數時,以下語句為合法的(以flDaemon為例):
- -d, --daemon
- -d=true, --daemon=true
- -d="true", --daemon="true"
- -d='true', --daemon='true'
當解析到第一個非定義的flag參數時,命令行flag參數解析工作結束。舉例說明,當執行docker命令docker --daemon=false --version=false ps時,flag參數解析主要完成兩個工作:
完成命令行flag參數的解析,根據flag的名稱-daemon和-version,得知具體的flag參數為flDaemon和flVersion,並獲得相應的值,均為false。
遇到第一個非定義的flag參數ps時,flag包會將ps及其之後所有的參數存入flag.Args(),以便之後執行Docker Client具體的請求時使用。
如需深入學習flag的實現,可以參見Docker源碼./docker/pkg/mflag/flag.go。
2.2.2 處理flag信息並收集Docker Client的配置信息
理解Go語言解析flag參數的相關知識,可以很大程度上幫助理解Docker的main函數的執行流程。通過總結,首先列出源碼中處理的flag信息以及收集Docker Client的配置信息,然後再一一進行分析:
處理的flag參數有:flVersion、flDebug、flDaemon、flTlsVerify以及flTls。
為Docker Client收集的配置信息有:protoAddrParts(通過flHosts參數獲得,作用是提供Docker Client與Docker Server的通信協議以及通信地址)、tlsConfig(通過一係列flag參數獲得,如*flTls、*flTlsVerify,作用是提供安全傳輸層協議的保障)。
清楚flag參數以及Docker Client的配置信息之後,我們進入main函數的源碼,具體分析如下。
在flag.Parse()之後的源碼如下:
if *flVersion {
showVersion()
return
}
以上代碼很好理解,解析flag參數後,若Docker發現flag參數flVersion為真,則說明Docker用戶希望查看Docker的版本信息。此時,Docker調用showVersion()顯示版本信息,並從main函數退出;否則的話,繼續往下執行。
if *flDebug {
os.Setenv("DEBUG", "1")
}
若flDebug參數為真的話,Docker通過os包中的Setenv函數創建一個名為DEBUG的環境變量,並將其值設為"1";繼續往下執行。
if len(flHosts) == 0 {
defaultHost := os.Getenv("DOCKER_HOST")
if defaultHost == "" || *flDaemon {
// If we do not have a host, default to unix socket
defaultHost = fmt.Sprintf("unix://%s", api.DEFAULTUNIXSOCKET)
}
if _, err := api.ValidateHost(defaultHost); err != nil {
log.Fatal(err)
}
flHosts = append(flHosts, defaultHost)
}
以上的源碼主要分析內部變量flHosts。flHosts的作用是為Docker Client提供所要連接的host對象,也就是為Docker Server提供所要監聽的對象。
在分析過程中,首先判斷flHosts變量是否長度為0。若是的話,則說明用戶並沒有顯性傳入地址,此時Docker的策略為選用默認值。Docker通過os包獲取名為DOCKER_HOST環境變量的值,將其賦值於defaultHost。若defaultHost為空或者flDaemon為真,說明目前還沒有一個定義的host對象,則將其默認設置為unix socket,值為api.DEFAULTUNIXSOCKET,該常量位於./docker/api/common.go,值為"/var/run/docker.sock",故defaultHost為"unix:///var/run/docker.sock"。驗證該defaultHost的合法性之後,將defaultHost的值追加至flHost的末尾,繼續往下執行。當然若flHost的長度不為0,則說明用戶已經指定地址,同樣繼續往下執行。
if *flDaemon {
mainDaemon()
return
}
若flDaemon參數為真,則說明用戶的需求是啟動Docker Daemon。Docker隨即執行mainDaemon函數,實現Docker Daemon的啟動;若mainDaemon函數執行完畢,則退出main函數。一般mainDaemon函數不會主動終結,Docker Daemon將作為一個常駐進程運行在宿主機上。本章著重介紹Docker Client的啟動,故假設flDaemon參數為假,不執行以上代碼塊。繼續往下執行。
if len(flHosts) > 1 {
log.Fatal("Please specify only one -H")
}
protoAddrParts := strings.SplitN(flHosts[0], "://", 2)
由於不執行Docker Daemon的啟動流程,故屬於Docker Client的執行邏輯。首先,判斷flHosts的長度是否大於1。若flHosts的長度大於1,則說明需要新創建的Docker Client訪問不止1個Docker Daemon地址,顯然邏輯上行不通,故拋出錯誤日誌,提醒用戶隻能指定一個Docker Daemon地址。接著,Docker將flHosts這個string數組中的第一個元素進行分割,通過"://"來分割,分割出的兩個部分放入變量protoAddrParts數組中。protoAddrParts的作用是:解析出Docker Client與Docker Server建立通信的協議與地址,為Docker Client創建過程中不可或缺的配置信息之一。一般情況下,flHosts[0]的值可以是tcp://0.0.0.0:2375或者unix:///var/run/docker.sock等。
var (
cli *client.DockerCli
tlsConfig tls.Config
)
tlsConfig.InsecureSkipVerify = true
由於之前已經假設過flDaemon為假,可以認定main函數的運行是為了Docker Client的創建與執行。Docker在這裏創建了兩個變量:一個為類型是*client.DockerCli的對象cli,另一個為類型是tls.Config的對象tlsConfig。定義完變量之後,Docker將tlsConfig的InsecureSkipVerify屬性置為真。tlsConfig對象的創建是為了保障cli在傳輸數據的時候遵循安全傳輸層協議(TLS)。安全傳輸層協議(TLS)用於確保兩個通信應用程序之間的保密性與數據完整性。tlsConfig是Docker Client創建過程中可選的配置信息。
// 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
}
若flTlsVerify這個flag參數為真,則說明Docker Client需Docker Server一起驗證連接的安全性。此時,tlsConfig對象需要加載一個受信的ca文件。該ca文件的路徑為*flCA參數的值,最終完成tlsConfig對象中RootCAs屬性的賦值,並將InsecureSkipVerify屬性置為假。
// 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}
}
}
如果flTls和flTlsVerify兩個flag參數中有一個為真,則說明需要加載並發送客戶端的證書。最終將證書內容交給tlsConfig的Certificates屬性。
至此,flag參數已經全部處理完畢,DockerClient也已經收集到所需的配置信息。下一節將主要分析如何創建Docker Client。
2.2.3 如何創建Docker Client
Docker Client的創建其實就是在已有配置參數信息的情況下,通過Client包中的NewDockerCli方法創建一個Docker Clinet實例cli。具體源碼實現如下:
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)
}
若flag參數flTls為真或者flTlsVerify為真,則說明需要使用TLS協議來保障傳輸的安全性,故創建Docker Client的時候,將tlsConfig參數傳入;否則,同樣創建Docker Client,隻不過tlsConfig為nil。
關於Client包中的NewDockerCli函數的實現,可以具體參見./docker/api/clie```javascript
nt/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,DockerClient與Docker Server的傳輸協議;addr,Docker Client需要訪問的host目標地址;tlsConfig,安全傳輸層協議的配置。若tlsConfig不為空,則說明需要使用安全傳輸層協議,DockerCli對象的scheme設置為“https”,另外還有關於輸入、輸出以及錯誤顯示的配置等。最終函數返回DockerCli對象。
通過調用client包中的NewDockerCli函數,程序最終創建了Docker Client,返回main包中的main函數之後,程序繼續往下執行。
最後更新:2017-06-21 15:02:10