五步助你成為優(yōu)秀的Docker代碼貢獻(xiàn)者

責(zé)任編輯:editor005

作者:孫科翻譯

2015-06-23 21:18:28

摘自:dockerone

開源漸成主流,越來越多的開發(fā)者想?yún)⑴c開源社區(qū)。而時(shí)下最火熱的Docker也許就是開發(fā)者入手開源項(xiàng)目的最好選擇

開源漸成主流,越來越多的開發(fā)者想?yún)⑴c開源社區(qū)。而時(shí)下最火熱的Docker也許就是開發(fā)者入手開源項(xiàng)目的最好選擇,它不僅是目前最流行的開源項(xiàng)目之一,而且在提交Issue方面的文檔和流程都是目前我見過的開源項(xiàng)目里最好的。本文主要介紹了如何入手開源項(xiàng)目,一些小經(jīng)驗(yàn)和小工具,一起來學(xué)習(xí)。

成為一個(gè)流行開源項(xiàng)目(如Docker)的貢獻(xiàn)者有如下好處:

你可以參與改進(jìn)很多人都在使用的項(xiàng)目,以此來獲得認(rèn)同感;

你可以與開源社區(qū)中的那些聰明絕頂?shù)娜送献?

你可以通過參與理解和改進(jìn)這個(gè)項(xiàng)目來使自己成為一名更加出色的程序員。

但是,從一個(gè)新的基準(zhǔn)代碼(codebase)入手絕對(duì)是一件恐怖的事情。目前,Docker已經(jīng)有相當(dāng)多的代碼了,哪怕是修復(fù)一個(gè)小問題,都需要閱讀大量的代碼,并理解這些部分是如何組合在一起的。

不過,它們也并不如你想象的那么困難。你可以根據(jù)Docker的貢獻(xiàn)者指南來完成環(huán)境的配置。然后按照如下5個(gè)簡(jiǎn)單的步驟,配合相關(guān)的代碼片段來深入代碼基。你所歷練的這些技能,都將會(huì)在你的編程生涯的每個(gè)新項(xiàng)目中派上用場(chǎng)。那么還等什么,我們這就開始。

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

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

此處為本文的提示,我們將會(huì)使用一個(gè)名為Sourcegraph的站點(diǎn),Docker團(tuán)隊(duì)就使用它完成在線檢索和代碼瀏覽,和你使用智能IDE所做的差不多。建議在閱讀本文時(shí),打開Sourcegraph放在一邊,以更好地跟上文章的進(jìn)度。

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

我們正在尋找對(duì)應(yīng)‘docker’命令的‘main’函數(shù),它是‘docker/docker/docker.go’中的一個(gè)文件。點(diǎn)擊搜索結(jié)果,我們會(huì)跳到其定義(如下所示)。花一點(diǎn)時(shí)間瀏覽一下這個(gè)函數(shù):

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’函數(shù)的頂部,我們看了許多與日志配置,命令標(biāo)志讀取以及默認(rèn)初始化相關(guān)的代碼。在底部,我們發(fā)現(xiàn)了對(duì)『client.NewDockerCli』的調(diào)用,它似乎是用來負(fù)責(zé)創(chuàng)建結(jié)構(gòu)體的,而這個(gè)結(jié)構(gòu)體的函數(shù)則會(huì)完成所有的實(shí)際工作。讓我們來搜索『NewDockerCli』。

步驟2:找到核心部分

在很多的應(yīng)用和程序庫中,都有1到2個(gè)關(guān)鍵接口,它表述了核心功能或者本質(zhì)。讓我們嘗試到達(dá)這個(gè)關(guān)鍵部分。

點(diǎn)擊‘NewDockerCli’的搜索結(jié)果,我們會(huì)到達(dá)函數(shù)的定義。由于我們感興趣的只是這個(gè)函數(shù)所返回的結(jié)構(gòu)體——「DockerCli」,因此讓我們點(diǎn)擊返回類型來跳轉(zhuǎn)到其定義。

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,

}

}

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

步驟3:更進(jìn)一步

既然我們已經(jīng)找到了‘DockerCli’,這個(gè)Docker客戶端的核心‘控制器’,接下來讓我們繼續(xù)深入,了解一條具體的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’的實(shí)現(xiàn)可以發(fā)現(xiàn),它調(diào)用了‘DockerCli.getMethod’方法來執(zhí)行每條Docker命令所對(duì)應(yīng)的函數(shù)。

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'. ", args[0])

os.Exit(1)

}

return method(args[1:]...)

}

return cli.CmdHelp()

}

在‘DockerCli.getMethod’中,我們可以看到它是通過對(duì)一個(gè)函數(shù)的動(dòng)態(tài)調(diào)用實(shí)現(xiàn)的,其中這個(gè)函數(shù)名的形式為在Docker命令前預(yù)置 “Cmd”字符串。那么在‘docker build’這個(gè)情況下,我們尋找的是‘DockerCli.CmdBuild’。但在這個(gè)文件中并沒有對(duì)應(yīng)的方法,因此讓我們需要搜索‘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

}

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

這里有很多內(nèi)容。在方法的頂部,我們可以看到代碼會(huì)為Dockerfile和配置處理各種輸入方法。通常,在閱讀一個(gè)很長的方法時(shí),倒過來讀是一種很不錯(cuò)的策略。從底部開始,觀察函數(shù)在最后做了什么。很多情況中,它們都是函數(shù)的本質(zhì),而之前的內(nèi)容無非只是用來補(bǔ)全核心行為的。

在‘CmdBuild’的底部,我們可以看到通過‘cli.stream’構(gòu)造的‘POST’請(qǐng)求。通過一些額外定義的跳轉(zhuǎn),我們到達(dá)了‘DockerCli.clientRequest’,它構(gòu)造一個(gè)HTTP請(qǐng)求,這個(gè)請(qǐng)求包含你通過‘docker build’傳遞給Docker的信息。因此在這里,‘docker build所做的就是發(fā)出一個(gè)設(shè)想的’POST‘請(qǐng)求給Docker守護(hù)進(jìn)程。如果你愿意,你也可以使用’curl‘來完成這個(gè)行為。

至此,我們已經(jīng)徹底了解了一個(gè)單獨(dú)的Docker客戶端命令,或許你仍希望更進(jìn)一步,找到守護(hù)進(jìn)程接受請(qǐng)求的部分,并一路跟蹤到它和LXC以及內(nèi)核交互的部分。這當(dāng)然是一條合理的路徑,但是我們將其作為練習(xí)留給各位讀者。接下來,讓我們對(duì)客戶端的關(guān)鍵組件有一個(gè)更加全面的認(rèn)識(shí)。

[page]

步驟4:查看使用示例

更好地理解一段代碼的方式是查看展示代碼如何被應(yīng)用的使用示例。讓我們回到'DockerCli.clientRequest'方法。在右手邊的Sourcegraph面板中,我們可以瀏覽這個(gè)方法的使用例子。結(jié)果顯示,這個(gè)方法在多處被使用,因?yàn)榇蟛糠諨ocker客戶端命令都會(huì)產(chǎn)生傳到守護(hù)進(jìn)程的HTTP請(qǐng)求。

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

請(qǐng)?jiān)诟嗟暮瘮?shù)和方法上嘗試,理解它們的內(nèi)部聯(lián)系。如果這有幫助,那么請(qǐng)就應(yīng)用的不同模塊如何交互,畫一張圖。

步驟5:選擇一個(gè)問題并開始coding

既然你已經(jīng)對(duì)Docker的代碼基有了一個(gè)大概的認(rèn)識(shí),那么可以查閱一下issue跟蹤系統(tǒng),看看哪些問題亟待解決,并在遇到你自己無法回答的問題時(shí),向Docker社區(qū)的成員申援。由于你已經(jīng)花了時(shí)間來摸索并理解代碼,那么你應(yīng)該已經(jīng)具備條件來提出“聰明”的問題,并知道問題大概出在哪里。

如果你覺得有必要,可以一路做好筆記,記錄你的經(jīng)歷,并像本文一樣作為博客發(fā)布。Docker團(tuán)隊(duì)會(huì)很樂意看到,你研究他們代碼的經(jīng)歷。

有效地貢獻(xiàn)

對(duì)一個(gè)巨大且陌生的基準(zhǔn)代碼的恐懼,儼然已經(jīng)成為了一個(gè)阻止人們參與到項(xiàng)目中的誤解。我們經(jīng)常假設(shè),對(duì)于程序員而言,工作的難點(diǎn)在于寫代碼,然而閱讀并理解他人的代碼卻往往是最關(guān)鍵的一步。認(rèn)識(shí)到這一切,并堅(jiān)定地迎接任務(wù),輔以優(yōu)秀的工具,會(huì)幫助你克服心理防線,以更好地投入到代碼中。

那么,開始動(dòng)手吧,檢查一下Docker今天的代碼。一個(gè)充滿活力的開源社區(qū)和基準(zhǔn)代碼正等著你!

鏈接已復(fù)制,快去分享吧

企業(yè)網(wǎng)版權(quán)所有?2010-2024 京ICP備09108050號(hào)-6京公網(wǎng)安備 11010502049343號(hào)