5步助你成為優質Docker貢獻者

jopen 9年前發布 | 8K 次閱讀 Docker

原文地址: https://sourcegraph.com/github.com/docker/docker

原作者: Charles Vickery

成為一個流行開源項目(如 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今天的代碼 。一個充滿活力的開源社區和代碼基正等著你!

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

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!