歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> 5 步助你成為一名優秀的 Docker 代碼貢獻者

5 步助你成為一名優秀的 Docker 代碼貢獻者

日期:2017/2/28 14:00:35   编辑:Linux教程

【編者的話】開源漸成主流,越來越多的開發者想參與開源社區。而時下最火熱的Docker也許就是開發者入手開源項目的最好選擇,它不僅是目前最流行的開源項目之一,而且在提交Issue方面的文檔和流程都是目前我見過的開源項目裡最好的。本文主要介紹了如何入手開源項目,一些小經驗和小工具,一起來學習。

成為一個流行開源項目(如Docker)的貢獻者有如下好處:

  • 你可以參與改進很多人都在使用的項目,以此來獲得認同感;
  • 你可以與開源社區中的那些聰明絕頂的人通力合作;
  • 你可以通過參與理解和改進這個項目來使自己成為一名更加出色的程序員。

但是,從一個新的基准代碼(codebase)入手絕對是一件恐怖的事情。目前,Docker已經有相當多的代碼了,哪怕是修復一個小問題,都需要閱讀大量的代碼,並理解這些部分是如何組合在一起的。

不過,它們也並不如你想象的那麼困難。你可以根據Docker的貢獻者指南來完成環境的配置。然後按照如下5個簡單的步驟,配合相關的代碼片段來深入代碼基。你所歷練的這些技能,都將會在你的編程生涯的每個新項目中派上用場。那麼還等什麼,我們這就開始。

步驟1:從'func main()'開始

正如一句古話所述,從你知道的開始。如果你和大部分Docker用戶一樣,你可能主要使用Docker CLI。因此,讓我們從程序的入口開始:‘main’函數。

此處為本文的提示,我們將會使用一個名為Sourcegraph的站點,Docker團隊就使用它完成在線檢索和代碼浏覽,和你使用智能IDE所做的差不多。建議在閱讀本文時,打開Sourcegraph放在一邊,以更好地跟上文章的進度。

在Sourcegraph站點,讓我們搜索Docker倉庫中的‘func main()’。

我們正在尋找對應‘docker’命令的‘main’函數,它是‘docker/docker/docker.go’中的一個文件。點擊搜索結果,我們會跳到其定義(如下所示)。花一點時間浏覽一下這個函數:

  1. func main(){
  2. if reexec.Init(){
  3. return
  4. }
  5. // Set terminal emulation based on platform as required.
  6. stdin, stdout, stderr := term.StdStreams()
  7. initLogging(stderr)
  8. flag.Parse()
  9. // FIXME: validate daemon flags here
  10. if*flVersion {
  11. showVersion()
  12. return
  13. }
  14. if*flLogLevel !=""{
  15. lvl, err := logrus.ParseLevel(*flLogLevel)
  16. if err !=nil{
  17. logrus.Fatalf("Unable to parse logging level: %s",*flLogLevel)
  18. }
  19. setLogLevel(lvl)
  20. }else{
  21. setLogLevel(logrus.InfoLevel)
  22. }
  23. // -D, --debug, -l/--log-level=debug processing
  24. // When/if -D is removed this block can be deleted
  25. if*flDebug {
  26. os.Setenv("DEBUG","1")
  27. setLogLevel(logrus.DebugLevel)
  28. }
  29. if len(flHosts)==0{
  30. defaultHost := os.Getenv("DOCKER_HOST")
  31. if defaultHost ==""||*flDaemon {
  32. // If we do not have a host, default to unix socket
  33. defaultHost = fmt.Sprintf("unix://%s", api.DEFAULTUNIXSOCKET)
  34. }
  35. defaultHost, err := api.ValidateHost(defaultHost)
  36. if err !=nil{
  37. logrus.Fatal(err)
  38. }
  39. flHosts = append(flHosts, defaultHost)
  40. }
  41. setDefaultConfFlag(flTrustKey, defaultTrustKeyFile)
  42. if*flDaemon {
  43. if*flHelp {
  44. flag.Usage()
  45. return
  46. }
  47. mainDaemon()
  48. return
  49. }
  50. if len(flHosts)>1{
  51. logrus.Fatal("Please specify only one -H")
  52. }
  53. protoAddrParts := strings.SplitN(flHosts[0],"://",2)
  54. var(
  55. cli *client.DockerCli
  56. tlsConfig tls.Config
  57. )
  58. tlsConfig.InsecureSkipVerify=true
  59. // Regardless of whether the user sets it to true or false, if they
  60. // specify --tlsverify at all then we need to turn on tls
  61. if flag.IsSet("-tlsverify"){
  62. *flTls =true
  63. }
  64. // If we should verify the server, we need to load a trusted ca
  65. if*flTlsVerify {
  66. certPool := x509.NewCertPool()
  67. file, err := ioutil.ReadFile(*flCa)
  68. if err !=nil{
  69. logrus.Fatalf("Couldn't read ca cert %s: %s",*flCa, err)
  70. }
  71. certPool.AppendCertsFromPEM(file)
  72. tlsConfig.RootCAs= certPool
  73. tlsConfig.InsecureSkipVerify=false
  74. }
  75. // If tls is enabled, try to load and send client certificates
  76. if*flTls ||*flTlsVerify {
  77. _, errCert := os.Stat(*flCert)
  78. _, errKey := os.Stat(*flKey)
  79. if errCert ==nil&& errKey ==nil{
  80. *flTls =true
  81. cert, err := tls.LoadX509KeyPair(*flCert,*flKey)
  82. if err !=nil{
  83. logrus.Fatalf("Couldn't load X509 key pair: %q. Make sure the key is encrypted", err)
  84. }
  85. tlsConfig.Certificates=[]tls.Certificate{cert}
  86. }
  87. // Avoid fallback to SSL protocols < TLS1.0
  88. tlsConfig.MinVersion= tls.VersionTLS10
  89. }
  90. if*flTls ||*flTlsVerify {
  91. cli = client.NewDockerCli(stdin, stdout, stderr,*flTrustKey, protoAddrParts[0], protoAddrParts[1],&tlsConfig)
  92. }else{
  93. cli = client.NewDockerCli(stdin, stdout, stderr,*flTrustKey, protoAddrParts[0], protoAddrParts[1],nil)
  94. }
  95. if err := cli.Cmd(flag.Args()...); err !=nil{
  96. if sterr, ok := err.(*utils.StatusError); ok {
  97. if sterr.Status!=""{
  98. logrus.Println(sterr.Status)
  99. }
  100. os.Exit(sterr.StatusCode)
  101. }
  102. logrus.Fatal(err)
  103. }
  104. }

在‘main’函數的頂部,我們看了許多與日志配置,命令標志讀取以及默認初始化相關的代碼。在底部,我們發現了對『client.NewDockerCli』的調用,它似乎是用來負責創建結構體的,而這個結構體的函數則會完成所有的實際工作。讓我們來搜索『NewDockerCli』。

步驟2:找到核心部分

在很多的應用和程序庫中,都有1到2個關鍵接口,它表述了核心功能或者本質。讓我們嘗試到達這個關鍵部分。

點擊‘NewDockerCli’的搜索結果,我們會到達函數的定義。由於我們感興趣的只是這個函數所返回的結構體——「DockerCli」,因此讓我們點擊返回類型來跳轉到其定義。

  1. func NewDockerCli(in io.ReadCloser,out, err io.Writer, keyFile string, proto, addr string, tlsConfig *tls.Config)*DockerCli{
  2. var(
  3. inFd uintptr
  4. outFd uintptr
  5. isTerminalIn =false
  6. isTerminalOut =false
  7. scheme ="http"
  8. )
  9. if tlsConfig !=nil{
  10. scheme ="https"
  11. }
  12. ifin!=nil{
  13. inFd, isTerminalIn = term.GetFdInfo(in)
  14. }
  15. ifout!=nil{
  16. outFd, isTerminalOut = term.GetFdInfo(out)
  17. }
  18. if err ==nil{
  19. err =out
  20. }
  21. // The transport is created here for reuse during the client session
  22. tr :=&http.Transport{
  23. TLSClientConfig: tlsConfig,
  24. }
  25. // Why 32? See issue 8035
  26. timeout :=32* time.Second
  27. if proto =="unix"{
  28. // no need in compressing for local communications
  29. tr.DisableCompression=true
  30. tr.Dial= func(_, _ string)(net.Conn, error){
  31. return net.DialTimeout(proto, addr, timeout)
  32. }
  33. }else{
  34. tr.Proxy= http.ProxyFromEnvironment
  35. tr.Dial=(&net.Dialer{Timeout: timeout}).Dial
  36. }
  37. return&DockerCli{
  38. proto: proto,
  39. addr: addr,
  40. in:in,
  41. out:out,
  42. err: err,
  43. keyFile: keyFile,
  44. inFd: inFd,
  45. outFd: outFd,
  46. isTerminalIn: isTerminalIn,
  47. isTerminalOut: isTerminalOut,
  48. tlsConfig: tlsConfig,
  49. scheme: scheme,
  50. transport: tr,
  51. }
  52. }

點擊『DockerCli』將我們帶到了它的定義。向下滾動這個文件,我們可以看到它的方法, ‘getMethod’,‘Cmd’,‘Subcmd’和‘LoadConfigFile’。其中,‘Cmd’值得留意。它是唯一一個包含docstring的方法,而docstring則表明它是執行每條Docker命令的核心方法。

步驟3:更進一步

既然我們已經找到了‘DockerCli’,這個Docker客戶端的核心‘控制器’,接下來讓我們繼續深入,了解一條具體的Docker命令是如何工作的。讓我們放大‘docker build’部分的代碼。

  1. type DockerClistruct{
  2. proto string
  3. addr string
  4. configFile *registry.ConfigFile
  5. in io.ReadCloser
  6. out io.Writer
  7. err io.Writer
  8. keyFile string
  9. tlsConfig *tls.Config
  10. scheme string
  11. // inFd holds file descriptor of the client's STDIN, if it's a valid file
  12. inFd uintptr
  13. // outFd holds file descriptor of the client's STDOUT, if it's a valid file
  14. outFd uintptr
  15. // isTerminalIn describes if client's STDIN is a TTY
  16. isTerminalIn bool
  17. // isTerminalOut describes if client's STDOUT is a TTY
  18. isTerminalOut bool
  19. transport *http.Transport
  20. }

閱讀‘DockerCli.Cmd’的實現可以發現,它調用了‘DockerCli.getMethod’方法來執行每條Docker命令所對應的函數。

  1. func (cli *DockerCli)Cmd(args ...string) error {
  2. if len(args)>1{
  3. method, exists := cli.getMethod(args[:2]...)
  4. if exists {
  5. return method(args[2:]...)
  6. }
  7. }
  8. if len(args)>0{
  9. method, exists := cli.getMethod(args[0])
  10. if!exists {
  11. fmt.Fprintf(cli.err,"docker: '%s' is not a docker command. See 'docker --help'.\n", args[0])
  12. os.Exit(1)
  13. }
  14. return method(args[1:]...)
  15. }
  16. return cli.CmdHelp()
  17. }

在‘DockerCli.getMethod’中,我們可以看到它是通過對一個函數的動態調用實現的,其中這個函數名的形式為在Docker命令前預置“Cmd”字符串。那麼在‘docker build’這個情況下,我們尋找的是‘DockerCli.CmdBuild’。但在這個文件中並沒有對應的方法,因此讓我們需要搜索‘CmdBuild’。

  1. func (cli *DockerCli) getMethod(args ...string)(func(...string) error,bool){
  2. camelArgs := make([]string, len(args))
  3. for i, s := range args {
  4. if len(s)==0{
  5. returnnil,false
  6. }
  7. camelArgs[i]= strings.ToUpper(s[:1])+ strings.ToLower(s[1:])
  8. }
  9. methodName :="Cmd"+ strings.Join(camelArgs,"")
  10. method := reflect.ValueOf(cli).MethodByName(methodName)
  11. if!method.IsValid(){
  12. returnnil,false
  13. }
  14. return method.Interface().(func(...string) error),true
  15. }

搜索結果顯示‘DockerCli’中確實有一個‘CmdBuild’方法,因此跳到它的定義部分。由於‘DockerCli.CmdBuild’的方法體過長,因此就不在本文中嵌入了,但是這裡有它的鏈接。

這裡有很多內容。在方法的頂部,我們可以看到代碼會為Dockerfile和配置處理各種輸入方法。通常,在閱讀一個很長的方法時,倒過來讀是一種很不錯的策略。從底部開始,觀察函數在最後做了什麼。很多情況中,它們都是函數的本質,而之前的內容無非只是用來補全核心行為的。

在‘CmdBuild’的底部,我們可以看到通過‘cli.stream’構造的‘POST’請求。通過一些額外定義的跳轉,我們到達了‘DockerCli.clientRequest’,它構造一個HTTP請求,這個請求包含你通過‘docker build’傳遞給Docker的信息。因此在這裡,‘docker build所做的就是發出一個設想的’POST‘請求給Docker守護進程。如果你願意,你也可以使用’curl‘來完成這個行為。

至此,我們已經徹底了解了一個單獨的Docker客戶端命令,或許你仍希望更進一步,找到守護進程接受請求的部分,並一路跟蹤到它和LXC以及內核交互的部分。這當然是一條合理的路徑,但是我們將其作為練習留給各位讀者。接下來,讓我們對客戶端的關鍵組件有一個更加全面的認識。

步驟4:查看使用示例

更好地理解一段代碼的方式是查看展示代碼如何被應用的使用示例。讓我們回到'DockerCli.clientRequest'方法。在右手邊的Sourcegraph面板中,我們可以浏覽這個方法的使用例子。結果顯示,這個方法在多處被使用,因為大部分Docker客戶端命令都會產生傳到守護進程的HTTP請求。

為了完全理解一個代碼片段,你需要同時知曉它是如何工作的以及是如何來使用的。通過閱讀代碼的定義部分讓我們理解前者,而查看使用示例則是涵蓋了後者。

請在更多的函數和方法上嘗試,理解它們的內部聯系。如果這有幫助,那麼請就應用的不同模塊如何交互,畫一張圖。

步驟5:選擇一個問題並開始coding

既然你已經對Docker的代碼基有了一個大概的認識,那麼可以查閱一下issue跟蹤系統,看看哪些問題亟待解決,並在遇到你自己無法回答的問題時,向Docker社區的成員申援。由於你已經花了時間來摸索並理解代碼,那麼你應該已經具備條件來提出“聰明”的問題,並知道問題大概出在哪裡。

如果你覺得有必要,可以一路做好筆記,記錄你的經歷,並像本文一樣作為博客發布。Docker團隊會很樂意看到,你研究他們代碼的經歷。

有效地貢獻

對一個巨大且陌生的基准代碼的恐懼,俨然已經成為了一個阻止人們參與到項目中的誤解。我們經常假設,對於程序員而言,工作的難點在於寫代碼,然而閱讀並理解他人的代碼卻往往是最關鍵的一步。認識到這一切,並堅定地迎接任務,輔以優秀的工具,會幫助你克服心理防線,以更好地投入到代碼中。

那麼,開始動手吧,檢查一下Docker今天的代碼。一個充滿活力的開源社區和基准代碼正等著你!

Docker安裝應用(CentOS 6.5_x64) http://www.linuxidc.com/Linux/2014-07/104595.htm

在 Docker 中使用 MySQL http://www.linuxidc.com/Linux/2014-01/95354.htm

在Ubuntu Trusty 14.04 (LTS) (64-bit)安裝Docker http://www.linuxidc.com/Linux/2014-10/108184.htm

Docker安裝應用(CentOS 6.5_x64) http://www.linuxidc.com/Linux/2014-07/104595.htm

Ubuntu 14.04安裝Docker http://www.linuxidc.com/linux/2014-08/105656.htm

阿裡雲CentOS 6.5 模板上安裝 Docker http://www.linuxidc.com/Linux/2014-11/109107.htm

Docker 的詳細介紹:請點這裡
Docker 的下載地址:請點這裡

Copyright © Linux教程網 All Rights Reserved