5步助你成为优质Docker贡献者

jopen 9年前

原文地址: 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以及内核交互的部分。这是一条合理的路径,但是我们这里就将其作为练习留给各位读者。接下来,让我们对客户端的关键组件有一个更加全面的理解。