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

Charles Vickery發表於2015-06-24


成為一個流行開源專案(如Docker)的貢獻者有如下好處:
  • 你可以參與改進很多人都在使用的專案,以此來獲得認同感;
  • 你可以與開源社群中的那些聰明絕頂的人通力合作;
  • 你可以通過參與理解和改進這個專案來使自己成為一名更加出色的程式設計師
但是,從一個新的基準程式碼(codebase)入手絕對是一件恐怖的事情。目前,Docker已經有相當多的程式碼了,哪怕是修復一個小問題,都需要閱讀大量的程式碼,並理解這些部分是如何組合在一起的。

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

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

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

此處為本文的提示,我們將會使用一個名為Sourcegraph的站點,Docker團隊就使用它完成線上檢索和程式碼瀏覽,和你使用智慧IDE所做的差不多。建議在閱讀本文時,開啟Sourcegraph放在一邊,以更好地跟上文章的進度。
在Sourcegraph站點,讓我們搜尋Docker倉庫中的‘func main()’。
5 步助你成為一名優秀的 Docker 程式碼貢獻者
我們正在尋找對應‘docker’命令的‘main’函式,它是‘docker/docker/docker.go’中的一個檔案。點選搜尋結果,我們會跳到其定義(如下所示)。花一點時間瀏覽一下這個函式:
func main() {
        if reexec.Init() {
                return
        }

        // Set terminal emulation based on platform as required.
        stdin, stdout, stderr := term.StdStreams()

        initLogging(stderr)

        flag.Parse()
        // FIXME: validate daemon flags here

        if *flVersion {
                showVersion()
                return
        }

        if *flLogLevel != "" {
                lvl, err := logrus.ParseLevel(*flLogLevel)
                if err != nil {
                        logrus.Fatalf("Unable to parse logging level: %s", *flLogLevel)
                }
                setLogLevel(lvl)
        } else {
                setLogLevel(logrus.InfoLevel)
        }

        // -D, --debug, -l/--log-level=debug processing
        // When/if -D is removed this block can be deleted
        if *flDebug {
                os.Setenv("DEBUG", "1")
                setLogLevel(logrus.DebugLevel)
        }

        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)
                }
                defaultHost, err := api.ValidateHost(defaultHost)
                if err != nil {
                        logrus.Fatal(err)
                }
                flHosts = append(flHosts, defaultHost)
        }

        setDefaultConfFlag(flTrustKey, defaultTrustKeyFile)

        if *flDaemon {
                if *flHelp {
                        flag.Usage()
                        return
                }
                mainDaemon()
                return
        }

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

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

        // Regardless of whether the user sets it to true or false, if they
        // specify --tlsverify at all then we need to turn on tls
        if flag.IsSet("-tlsverify") {
                *flTls = true
        }

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

        // 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 {
                                logrus.Fatalf("Couldn't load X509 key pair: %q. Make sure the key is encrypted", err)
                        }
                        tlsConfig.Certificates = []tls.Certificate{cert}
                }
                // Avoid fallback to SSL protocols < TLS1.0
                tlsConfig.MinVersion = tls.VersionTLS10
        }

        if *flTls || *flTlsVerify {
                cli = client.NewDockerCli(stdin, stdout, stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], &tlsConfig)
        } else {
                cli = client.NewDockerCli(stdin, stdout, stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], nil)
        }

        if err := cli.Cmd(flag.Args()...); err != nil {
                if sterr, ok := err.(*utils.StatusError); ok {
                        if sterr.Status != "" {
                                logrus.Println(sterr.Status)
                        }
                        os.Exit(sterr.StatusCode)
                }
                logrus.Fatal(err)
        }
}
在‘main’函式的頂部,我們看了許多與日誌配置,命令標誌讀取以及預設初始化相關的程式碼。在底部,我們發現了對『client.NewDockerCli』的呼叫,它似乎是用來負責建立結構體的,而這個結構體的函式則會完成所有的實際工作。讓我們來搜尋『NewDockerCli』

步驟2:找到核心部分

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

點選‘NewDockerCli’的搜尋結果,我們會到達函式的定義。由於我們感興趣的只是這個函式所返回的結構體——「DockerCli」,因此讓我們點選返回型別來跳轉到其定義。
func NewDockerCli(in io.ReadCloser, out, err io.Writer, keyFile string, proto, addr string, tlsConfig *tls.Config) *DockerCli {
        var (
                inFd          uintptr
                outFd         uintptr
                isTerminalIn  = false
                isTerminalOut = false
                scheme        = "http"
        )

        if tlsConfig != nil {
                scheme = "https"
        }
        if in != nil {
                inFd, isTerminalIn = term.GetFdInfo(in)
        }

        if out != nil {
                outFd, isTerminalOut = term.GetFdInfo(out)
        }

        if err == nil {
                err = out
        }

        // The transport is created here for reuse during the client session
        tr := &http.Transport{
                TLSClientConfig: tlsConfig,
        }

        // Why 32? See issue 8035
        timeout := 32 * time.Second
        if proto == "unix" {
                // no need in compressing for local communications
                tr.DisableCompression = true
                tr.Dial = func(_, _ string) (net.Conn, error) {
                        return net.DialTimeout(proto, addr, timeout)
                }
        } else {
                tr.Proxy = http.ProxyFromEnvironment
                tr.Dial = (&net.Dialer{Timeout: timeout}).Dial
        }

        return &DockerCli{
                proto:         proto,
                addr:          addr,
                in:            in,
                out:           out,
                err:           err,
                keyFile:       keyFile,
                inFd:          inFd,
                outFd:         outFd,
                isTerminalIn:  isTerminalIn,
                isTerminalOut: isTerminalOut,
                tlsConfig:     tlsConfig,
                scheme:        scheme,
                transport:     tr,
        }
}
點選『DockerCli』將我們帶到了它的定義。向下滾動這個檔案,我們可以看到它的方法, ‘getMethod’,‘Cmd’,‘Subcmd’和‘LoadConfigFile’。其中,‘Cmd’值得留意。它是唯一一個包含docstring的方法,而docstring則表明它是執行每條Docker命令的核心方法。

步驟3:更進一步

既然我們已經找到了‘DockerCli’,這個Docker客戶端的核心‘控制器’,接下來讓我們繼續深入,瞭解一條具體的Docker命令是如何工作的。讓我們放大‘docker build’部分的程式碼。
type DockerCli struct {
        proto      string
        addr       string
        configFile *registry.ConfigFile
        in         io.ReadCloser
        out        io.Writer
        err        io.Writer
        keyFile    string
        tlsConfig  *tls.Config
        scheme     string
        // inFd holds file descriptor of the client's STDIN, if it's a valid file
        inFd uintptr
        // outFd holds file descriptor of the client's STDOUT, if it's a valid file
        outFd uintptr
        // isTerminalIn describes if client's STDIN is a TTY
        isTerminalIn bool
        // isTerminalOut describes if client's STDOUT is a TTY
        isTerminalOut bool
        transport     *http.Transport
}
閱讀‘DockerCli.Cmd’的實現可以發現,它呼叫了‘DockerCli.getMethod’方法來執行每條Docker命令所對應的函式。
func (cli *DockerCli) Cmd(args ...string) error {
        if len(args) > 1 {
                method, exists := cli.getMethod(args[:2]...)
                if exists {
                        return method(args[2:]...)
                }
        }
        if len(args) > 0 {
                method, exists := cli.getMethod(args[0])
                if !exists {
                        fmt.Fprintf(cli.err, "docker: '%s' is not a docker command. See 'docker --help'./n", args[0])
                        os.Exit(1)
                }
                return method(args[1:]...)
        }
        return cli.CmdHelp()
}
在‘DockerCli.getMethod’中,我們可以看到它是通過對一個函式的動態呼叫實現的,其中這個函式名的形式為在Docker命令前預置“Cmd”字串。那麼在‘docker build’這個情況下,我們尋找的是‘DockerCli.CmdBuild’。但在這個檔案中並沒有對應的方法,因此讓我們需要搜尋‘CmdBuild’
func (cli *DockerCli) getMethod(args ...string) (func(...string) error, bool) {
        camelArgs := make([]string, len(args))
        for i, s := range args {
                if len(s) == 0 {
                        return nil, false
                }
                camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
        }
        methodName := "Cmd" + strings.Join(camelArgs, "")
        method := reflect.ValueOf(cli).MethodByName(methodName)
        if !method.IsValid() {
                return nil, false
        }
        return method.Interface().(func(...string) error), true
}
搜尋結果顯示‘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 步助你成為一名優秀的 Docker 程式碼貢獻者
為了完全理解一個程式碼片段,你需要同時知曉它是如何工作的以及是如何來使用的。通過閱讀程式碼的定義部分讓我們理解前者,而檢視使用示例則是涵蓋了後者。

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

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

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

如果你覺得有必要,可以一路做好筆記,記錄你的經歷,並像本文一樣作為部落格釋出。Docker團隊會很樂意看到,你研究他們程式碼的經歷。

有效地貢獻

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

那麼,開始動手吧,檢查一下Docker今天的程式碼。一個充滿活力的開源社群和基準程式碼正等著你!
評論(1)

相關文章