Docker源碼分析(二):Docker Client建立與命令執行

1. 前言

現在,Docker做爲業界領先的輕量級虛擬化容器管理引擎,給全球開發者提供了一種新穎、便捷的軟件集成測試與部署之道。在團隊開發軟件時,Docker能夠提供可複用的運行環境、靈活的資源配置、便捷的集成測試方法以及一鍵式的部署方式。能夠說,Docker的優點在簡化持續集成、運維部署方面體現得淋漓盡致,它徹底讓開發者從持續集成、運維部署方面中解放出來,把精力真正地傾注在開發上。git

然而,把Docker的功能發揮到極致,並不是一件易事。在深入理解Docker架構的狀況下,熟練掌握Docker Client的使用也很是有必要。前者能夠參閱《Docker源碼分析》系列之Docker架構篇,而本文主要針對後者,從源碼的角度分析Docker Client,力求幫助開發者更深入的理解Docker Client的具體實現,最終更好的掌握Docker Client的使用方法。即本文爲《Docker源碼分析》系列的第二篇——Docker Client篇。github

2. Docker Client源碼分析章節安排

本文從源碼的角度,主要分析Docker Client的兩個方面:建立與命令執行。整個分析過程能夠分爲兩個部分:golang

第一部分分析Docker Client的建立。這部分的分析可分爲如下三個步驟:docker

  • 分析如何經過docker命令,解析出命令行flag參數,以及docker命令中的請求參數;
  • 分析如何處理具體的flag參數信息,並收集Docker Client所需的配置信息;
  • 分析如何建立一個Docker Client。

第二部分在已有Docker Client的基礎上,分析如何執行docker命令。這部分的分析又可分爲如下兩個步驟:json

  • 分析如何解析docker命令中的請求參數,獲取相應請求的類型;
  • 分析Docker Client如何執行具體的請求命令,最終將請求發送至Docker Server。

3. Docker Client的建立

Docker Client的建立,實質上是Docker用戶經過可執行文件docker,與Docker Server創建聯繫的客戶端。如下分三個小節分別闡述Docker Client的建立流程。api

如下爲整個docker源代碼運行的流程圖:數組

上圖經過流程圖的方式,使得讀者更爲清晰的瞭解Docker Client建立及執行請求的過程。其中涉及了諸多源代碼中的特有名詞,在下文中會一一解釋與分析。安全

3.1. Docker命令的flag參數解析

衆所周知,在Docker的具體實現中,Docker Server與Docker Client均由可執行文件docker來完成建立並啓動。那麼,瞭解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 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()方法的返回值,若爲真,則直接退出運行,不然的話繼續執行。查看位於./docker/reexec/reexec.go中reexec.Init()的定義,能夠發現因爲在docker運行以前沒有任何的Initializer註冊,故該代碼段執行的返回值爲假。

緊接着,main函數經過調用flag.Parse()解析命令行中的flag參數。查看源碼能夠發現Docker在./docker/docker/flag.go中定義了多個flag參數,並經過init函數進行初始化。代碼以下:

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 mode use '' (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.")
}

這裏涉及到了Golang的一個特性,即init函數的執行。在Golang中init函數的特性以下:

  • init函數用於程序執行前包的初始化工做,好比初始化變量等;
  • 每一個包能夠有多個init函數;
  • 包的每個源文件也能夠有多個init函數;
  • 同一個包內的init函數的執行順序沒有明確的定義;
  • 不一樣包的init函數按照包導入的依賴關係決定初始化的順序;
  • init函數不能被調用,而是在main函數調用前自動被調用。

所以,在main函數執行以前,Docker已經定義了諸多flag參數,並對不少flag參數進行初始化。定義的命令行flag參數有:flVersion、flDaemon、flDebug、flSocketGroup、flEnableCors、flTls、flTlsVerify、flCa、flCert、flKey等。

如下具體分析flDaemon:

  • 定義:flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode")
  • flDaemon的類型爲Bool類型
  • flDaemon名稱爲」d」或者」-daemon」,該名稱會出如今docker命令中
  • flDaemon的默認值爲false
  • flDaemon的幫助信息爲」Enable daemon mode」
  • 訪問flDaemon的值時,使用指針* flDaemon解引用訪問

在解析命令行flag參數時,如下的語言爲合法的:

  • -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參數的解析,名爲-daemon和-version的flag參數flDaemon和flVersion分別得到相應的值,均爲false;
  • 遇到第一個非flag參數的參數ps時,將ps及其以後全部的參數存入flag.Args(),以便以後執行Docker Client具體的請求時使用。

如需深刻學習flag的解析,能夠參見源碼命令行參數flag的解析

3.2. 處理flag信息並收集Docker Client的配置信息

有了以上flag參數解析的相關知識,分析Docker的main函數就變得簡單易懂不少。經過總結,首先列出源代碼中處理的flag信息以及收集Docker Client的配置信息,而後再一一對此分析:

  • 處理的flag參數有:flVersion、flDebug、flDaemon、flTlsVerify以及flTls;
  • 爲Docker Client收集的配置信息有:protoAddrParts(經過flHosts參數得到,做用爲提供Docker Client與Server的通訊協議以及通訊地址)、tlsConfig(經過一系列flag參數得到,如*flTls、*flTlsVerify,做用爲提供安全傳輸層協議的保障)。

隨即分析處理這些flag參數信息,以及配置信息。

在flag.Parse()以後的代碼以下:

  if *flVersion {
    showVersion()
    return
  }

不難理解的是,當通過解析flag參數後,若flVersion參數爲真時,調用showVersion()顯示版本信息,並從main函數退出;不然的話,繼續往下執行。

  if *flDebug {
    os.Setenv("DEBUG", "1")
  }

若flDebug參數爲真的話,經過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,如果的話,經過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的末尾。繼續往下執行。

  if *flDaemon {
    mainDaemon()
    return
  }

若flDaemon參數爲真的話,則執行mainDaemon函數,實現Docker Daemon的啓動,若mainDaemon函數執行完畢,則退出main函數,通常mainDaemon函數不會主動終結。因爲本章節介紹Docker Client的啓動,故假設flDaemon參數爲假,不執行以上代碼塊。繼續往下執行。

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

以上,若flHosts的長度大於1的話,則拋出錯誤日誌。接着將flHosts這個string數組中的第一個元素,進行分割,經過」://」來分割,分割出的兩個部分放入變量protoAddrParts數組中。protoAddrParts的做用爲解析出與Docker Server創建通訊的協議與地址,爲Docker Client建立過程當中不可或缺的配置信息之一。

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

因爲以前已經假設過flDaemon爲假,則能夠認定main函數的運行是爲了Docker Client的建立與執行。在這裏建立兩個變量:一個爲類型是client.DockerCli指針的對象cli,另外一個爲類型是tls.Config的對象tlsConfig。並將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參數爲真的話,則說明須要驗證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參數中有一個爲真,則說明須要加載以及發送client端的證書。最終將證書內容交給tlsConfig的Certificates屬性。

至此,flag參數已經所有處理,並已經收集完畢Docker Client所需的配置信息。以後的內容爲Docker Client如何實現建立並執行。

3.3. Docker Client的建立

Docker Client的建立其實就是在已有配置參數信息的狀況,經過Client包中的NewDockerCli方法建立一個實例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/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」,另外還有關於輸入,輸出以及錯誤顯示的配置,最終返回該對象。

經過調用NewDockerCli函數,程序最終完成了建立Docker Client,並返回main函數繼續執行。

4. Docker命令執行

main函數執行到目前爲止,有如下內容須要爲Docker命令的執行服務:建立完畢的Docker Client,docker命令中的請求參數(經flag解析後存放於flag.Arg())。也就是說,須要使用Docker Client來分析docker 命令中的請求參數,並最終發送相應請求給Docker Server。

4.1. Docker Client解析請求命令

Docker Client解析請求命令的工做,在Docker命令執行部分第一個完成,直接進入main函數以後的源碼部分

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)
  }

查閱以上源碼,能夠發現,正如以前所說,首先解析存放於flag.Args()中的具體請求參數,執行的函數爲cli對象的Cmd函數。進入./docker/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函數執行具體的指令。源碼實現中,首先判斷請求參數列表的長度是否大於0,若不是的話,說明沒有請求信息,返回docker命令的Help信息;若長度大於0的話,說明有請求信息,則首先經過請求參數列表中的第一個元素args[0]來獲取具體的method的方法。若是上述method方法不存在,則返回docker命令的Help信息,若存在的話,調用具體的method方法,參數爲args[1]及其以後全部的請求參數。

仍是以一個具體的docker命令爲例,docker –daemon=false –version=false pull Name。經過以上的分析,能夠總結出如下操做流程:

(1) 解析flag參數以後,將docker請求參數」pull」和「Name」存放於flag.Args();

(2) 建立好的Docker Client爲cli,cli執行cli.Cmd(flag.Args()…);

在Cmd函數中,經過args[0]也就是」pull」,執行cli.getMethod(args[0]),獲取method的名稱;

(3) 在getMothod方法中,經過處理最終返回method的值爲」CmdPull」;

(4) 最終執行method(args[1:]…)也就是CmdPull(args[1:]…)。

4.2. Docker Client執行請求命令

上一節經過一系列的命令解析,最終找到了具體的命令的執行方法,本節內容主要介紹Docker Client如何經過該執行方法處理併發送請求。

因爲不一樣的請求內容不一樣,執行流程大體相同,本節依舊以一個例子來闡述其中的流程,例子爲:docker pull NAME。

Docker Client在執行以上請求命令的時候,會執行CmdPull函數,傳入參數爲args[1:]...。源碼具體爲./docker/api/client/command.go中的CmdPull函數

如下逐一分析CmdPull的源碼實現。

(1) 經過cli包中的Subcmd方法定義一個類型爲Flagset的對象cmd。

cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry")

(2) 給cmd對象定義一個類型爲String的flag,名爲」#t」或」#-tag」,初始值爲空。

tag := cmd.String([]string{"#t", "#-tag"}, "", "Download tagged image in a repository")

(3) 將args參數進行解析,解析過程當中,先提取出是否有符合tag這個flag的參數,如有,將其給賦值給tag參數,其他的參數存入cmd.NArg();若無的話,全部的參數存入cmd.NArg()中。

if err := cmd.Parse(args); err != nil {
return nil }

(4) 判斷通過flag解析後的參數列表,若參數列表中參數的個數不爲1,則說明須要pull多個image,pull命令不支持,則調用錯誤處理方法cmd.Usage(),並返回nil。

if cmd.NArg() != 1 {
cmd.Usage()
return nil
    }

(5) 建立一個map類型的變量v,該變量用於存放pull鏡像時所需的url參數;隨後將參數列表的第一個值賦給remote變量,並將remote做爲鍵爲fromImage的值添加至v;最後如有tag信息的話,將tag信息做爲鍵爲」tag」的值添加至v。

var (
  v      = url.Values{}
  remote = cmd.Arg(0)
)
v.Set("fromImage", remote)
if *tag == "" {
  v.Set("tag", *tag)
}

(6) 經過remote變量解析出鏡像所在的host地址,以及鏡像的名稱。

  remote, _ = parsers.ParseRepositoryTag(remote)
    // Resolve the Repository name from fqn to hostname + name
    hostname, _, err := registry.ResolveRepositoryName(remote)
    if err != nil {
      return err
    }

 

(7) 經過cli對象獲取與Docker Server通訊所須要的認證配置信息。

cli.LoadConfigFile()
    // Resolve the Auth config relevant for this server
    authConfig := cli.configFile.ResolveAuthConfig(hostname)

(8) 定義一個名爲pull的函數,傳入的參數類型爲registry.AuthConfig,返回類型爲error。函數執行塊中最主要的內容爲:cli.stream(……)部分。該部分具體發起了一個給Docker Server的POST請求,請求的url爲"/images/create?"+v.Encode(),請求的認證信息爲:map[string][]string{"X-Registry-Auth": registryAuthHeader,}。

   pull := func(authConfig registry.AuthConfig) error {
      buf, err := json.Marshal(authConfig)
      if err != nil {
        return err
      }
      registryAuthHeader := []string{
        base64.URLEncoding.EncodeToString(buf),
      }
      return cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out, map[string][]string{
      "  X-Registry-Auth": registryAuthHeader,
      })
    }

(9) 因爲上一個步驟只是定義pull函數,這一步驟具體調用執行pull函數,若成功則最終返回,若返回錯誤,則作相應的錯誤處理。若返回錯誤爲401,則須要先登陸,轉至登陸環節,完成以後,繼續執行pull函數,若完成則最終返回。

 if err := pull(authConfig); err != nil {
  if strings.Contains(err.Error(), "Status 401") {
    fmt.Fprintln(cli.out, "\nPlease login prior to pull:")
    if err := cli.CmdLogin(hostname); err != nil {
      return err
    }
        authConfig := cli.configFile.ResolveAuthConfig(hostname)
        return pull(authConfig)
  }
  return err
}

以上即是pull請求的所有執行過程,其餘請求的執行在流程上也是大同小異。總之,請求執行過程當中,大多都是將命令行中關於請求的參數進行初步處理,並添加相應的輔助信息,最終經過指定的協議給Docker Server發送Docker Client和Docker Server約定好的API請求。

5. 總結

本文從源碼的角度分析了從docker可執行文件開始,到建立Docker Client,最終發送給Docker Server請求的完整過程。

筆者認爲,學習與理解Docker Client相關的源碼實現,不只可讓用戶熟練掌握Docker命令的使用,還可使得用戶在特殊狀況下有能力修改Docker Client的源碼,使其知足自身系統的某些特殊需求,以達到定製Docker Client的目的,最大發揮Docker開放思想的價值

 

6. 參考文獻

  1. http://www.infoq.com/cn/articles/docker-command-line-quest
  2. http://docs.studygolang.com/pkg/
  3. http://blog.studygolang.com/2013/02/%E6%A0%87%E5%87%86%E5%BA%93-%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%8F%82%E6%95%B0%E8%A7%A3%E6%9E%90flag/
  4. https://docs.docker.com/reference/commandline/cli/
相關文章
相關標籤/搜索