前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >golang 源码分析(17):cobra docker

golang 源码分析(17):cobra docker

作者头像
golangLeetcode
发布2022-08-02 16:35:56
5000
发布2022-08-02 16:35:56
举报
文章被收录于专栏:golang算法架构leetcode技术php

main函数的头两句

代码语言:javascript
复制
stdin, stdout, stderr := term.StdStreams()
logrus.SetOutput(stderr)

取得了终端的标准输入、标准输出、标准错误,并将日志输出至标准错误。

代码语言:javascript
复制
dockerCli := command.NewDockerCli(stdin, stdout, stderr)

然后创建DockerCli对象,DockerCli对象在cli/cli.go里声明。

代码语言:javascript
复制
cmd := newDockerCommand(dockerCli)

然后创建DockerCommand对象,这个是github.com/spf13/cobra库里所提及的所有命令的根命令。

代码语言:javascript
复制
	if err := cmd.Execute(); err != nil {
		if sterr, ok := err.(cli.StatusError); ok {
			if sterr.Status != "" {
				fmt.Fprintln(stderr, sterr.Status)
			}
			// StatusError should only be used for errors, and all errors should
			// have a non-zero exit status, so never exit with 0
			if sterr.StatusCode == 0 {
				os.Exit(1)
			}
			os.Exit(sterr.StatusCode)
		}
		fmt.Fprintln(stderr, err)
		os.Exit(1)
	}

最后执行命令,如果有错误则打印到标准输出里,然后退出。

看了下main函数,大家肯定知道关键代码肯定在cmd := newDockerCommand(dockerCli)这里。再来看newDockerCommand函数。

代码语言:javascript
复制
opts := cliflags.NewClientOptions()
var flags *pflag.FlagSet

这里首先创建了一个ClientOptions对象,一个*pflag.FlagSet对象

代码语言:javascript
复制
	cmd := &cobra.Command{
		Use:              "docker [OPTIONS] COMMAND [arg...]",
		Short:            "A self-sufficient runtime for containers.",
		SilenceUsage:     true,
		SilenceErrors:    true,
		TraverseChildren: true,
		Args:             noArgs,
		RunE: func(cmd *cobra.Command, args []string) error {
			if opts.Version {
				showVersion()
				return nil
			}
			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
			return nil
		},
		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
			// flags must be the top-level command flags, not cmd.Flags()
			opts.Common.SetDefaultOptions(flags)
			dockerPreRun(opts)
			return dockerCli.Initialize(opts)
		},
	}
	cli.SetupRootCommand(cmd)

然后构造了一个github.com/spf13/cobra库里所提及的根命令,当用户执行docker命令,并且不匹配其它子命令时,则这个根命令将得到执行,也即打印docker命令的用法。再使用cli.SetupRootCommand(cmd)初始化根命令。这个方法在cli/cobra.go里声明。

这里要提一下github.com/spf13/cobra库的工作原理。github.com/spf13/cobra库将一个命令行工具的所有命令抽象为一个层次结构,最上层为根命令,每个命令又可以定义它的子命令。每个命令在定义时可设置它的描述性文字,支持的选项、用法描述、命令的执行逻辑、相关模板等。用户执行命令行时,会根据命令行参数自动查找对应的命令,然后就可以运行该命令的执行逻辑了。详细用法可参阅github.com/spf13/cobra库的文档

代码语言:javascript
复制
	flags = cmd.Flags()
	flags.BoolVarP(&opts.Version, "version", "v", false, "Print version information and quit")
	flags.StringVar(&opts.ConfigDir, "config", cliconfig.ConfigDir(), "Location of client config files")
	opts.Common.InstallFlags(flags)

这些是一些命令行参数的定义。

代码语言:javascript
复制
	cmd.SetOutput(dockerCli.Out())

设置命令的输出为DockerCli的输出。

代码语言:javascript
复制
cmd.AddCommand(newDaemonCommand())

DaemonCommand添加为根命令的子命令,这样docker daemon命令即可启动docker daemon。代码里也说到这个特性以后会移除的,所以这个命令的Hidden被设置为了true,即显示命令用法时,并不会显示它。newDaemonCommand函数定义在cmd/docker/daemon_unix.go里。

代码语言:javascript
复制
commands.AddCommands(cmd, dockerCli)

将其它子命令添加至根命令,commands.AddCommands函数定义在cli/command/commands/commands.go里。

代码语言:javascript
复制
func AddCommands(cmd *cobra.Command, dockerCli *command.DockerCli) {
	cmd.AddCommand(
		node.NewNodeCommand(dockerCli),
		service.NewServiceCommand(dockerCli),
		stack.NewStackCommand(dockerCli),
		stack.NewTopLevelDeployCommand(dockerCli),
		swarm.NewSwarmCommand(dockerCli),
		container.NewContainerCommand(dockerCli),
		image.NewImageCommand(dockerCli),
		system.NewSystemCommand(dockerCli),
		container.NewRunCommand(dockerCli),
		image.NewBuildCommand(dockerCli),
		network.NewNetworkCommand(dockerCli),
		hide(system.NewEventsCommand(dockerCli)),
		registry.NewLoginCommand(dockerCli),
		registry.NewLogoutCommand(dockerCli),
		registry.NewSearchCommand(dockerCli),
		system.NewVersionCommand(dockerCli),
		volume.NewVolumeCommand(dockerCli),
		hide(system.NewInfoCommand(dockerCli)),
		hide(container.NewAttachCommand(dockerCli)),
...
		hide(system.NewInspectCommand(dockerCli)),
		checkpoint.NewCheckpointCommand(dockerCli),
		plugin.NewPluginCommand(dockerCli),
	)
}

可以看到这里定义了很多子命令,并添加为根命令的子命令,每个子命令构建时都将DockerCli对象传入了。同样为了保证兼容性的,对其它不少子命令用的hide函数对原有命令进行了处理,将其Hidden属性设置为了true

代码语言:javascript
复制
	return cmd

添加好子命令后,newDockerCommand函数就返回这个根命令退出了。

Client命令行示例

这里我拿一个非常简单的子命令示例,来说明Docker客户端是如何运行的。

比如执行docker system info命令,根据子命令定义,首先找到了system.NewSystemCommand函数,它是在cli/command/system/cmd.go里定义的。

代码语言:javascript
复制
func NewSystemCommand(dockerCli *command.DockerCli) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "system",
		Short: "Manage Docker",
		Args:  cli.NoArgs,
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
		},
	}
	cmd.AddCommand(
		NewEventsCommand(dockerCli),
		NewInfoCommand(dockerCli),
		NewDiskUsageCommand(dockerCli),
		NewPruneCommand(dockerCli),
	)
	return cmd
}

又由于子命令info,所以找到NewInfoCommand函数,这是在cli/command/system/info.go里定义的。

代码语言:javascript
复制
// NewInfoCommand creates a new cobra.Command for `docker info`
func NewInfoCommand(dockerCli *command.DockerCli) *cobra.Command {
	var opts infoOptions
	cmd := &cobra.Command{
		Use:   "info [OPTIONS]",
		Short: "Display system-wide information",
		Args:  cli.NoArgs,
		RunE: func(cmd *cobra.Command, args []string) error {
			return runInfo(dockerCli, &opts)
		},
	}
	flags := cmd.Flags()
	flags.StringVarP(&opts.format, "format", "f", "", "Format the output using the given go template")
	return cmd
}
func runInfo(dockerCli *command.DockerCli, opts *infoOptions) error {
	ctx := context.Background()
	info, err := dockerCli.Client().Info(ctx)
	if err != nil {
		return err
	}
	if opts.format == "" {
		return prettyPrintInfo(dockerCli, info)
	}
	return formatInfo(dockerCli, info, opts.format)
}

找到了匹配的子命令后,当命令等到执行时,该命令的RunE属性就会得到调用,即会调用runInfo函数,这个函数会调用dockerCli.Client().Info函数,并将输出结果格式化并写到DockerCli的输出。

Info(ctx context.Context) (types.Info, error)是一个接口,定义在client/interface.go里,其实现定义在client/info.go里。

代码语言:javascript
复制
func (cli *Client) Info(ctx context.Context) (types.Info, error) {
	var info types.Info
	serverResp, err := cli.get(ctx, "/info", url.Values{}, nil)
	if err != nil {
		return info, err
	}
	defer ensureReaderClosed(serverResp)
	if err := json.NewDecoder(serverResp.body).Decode(&info); err != nil {
		return info, fmt.Errorf("Error reading remote info: %v", err)
	}
	return info, nil
}

上述代码就比较简单了,就是向docker daemon里的api服务发送了一个get请求,并将响应结果用json解码,最终返回info。

再看看cli.get函数,这个定义在client/request.go,说白了就是发送了一个HTTP请求,不解释。

代码语言:javascript
复制
// getWithContext sends an http request to the docker API using the method GET with a specific go context.
func (cli *Client) get(ctx context.Context, path string, query url.Values, headers map[string][]string) (serverResponse, error) {
	return cli.sendRequest(ctx, "GET", path, query, nil, headers)
}
func (cli *Client) sendRequest(ctx context.Context, method, path string, query url.Values, obj interface{}, headers map[string][]string) (serverResponse, error) {
	var body io.Reader
	if obj != nil {
		var err error
		body, err = encodeData(obj)
		if err != nil {
			return serverResponse{}, err
		}
		if headers == nil {
			headers = make(map[string][]string)
		}
		headers["Content-Type"] = []string{"application/json"}
	}
	return cli.sendClientRequest(ctx, method, path, query, body, headers)
}
func (cli *Client) sendClientRequest(ctx context.Context, method, path string, query url.Values, body io.Reader, headers map[string][]string) (serverResponse, error) {
	serverResp := serverResponse{
		body:       nil,
		statusCode: -1,
	}
	expectedPayload := (method == "POST" || method == "PUT")
	if expectedPayload && body == nil {
		body = bytes.NewReader([]byte{})
	}
	req, err := cli.newRequest(method, path, query, body, headers)
	if err != nil {
		return serverResp, err
	}
	if cli.proto == "unix" || cli.proto == "npipe" {
		// For local communications, it doesn't matter what the host is. We just
		// need a valid and meaningful host name. (See #189)
		req.Host = "docker"
	}
	scheme, err := resolveScheme(cli.client.Transport)
	if err != nil {
		return serverResp, err
	}
	req.URL.Host = cli.addr
	req.URL.Scheme = scheme
	if expectedPayload && req.Header.Get("Content-Type") == "" {
		req.Header.Set("Content-Type", "text/plain")
	}
	resp, err := ctxhttp.Do(ctx, cli.client, req)
	if err != nil {
		if scheme == "https" && strings.Contains(err.Error(), "malformed HTTP response") {
			return serverResp, fmt.Errorf("%v.\n* Are you trying to connect to a TLS-enabled daemon without TLS?", err)
		}
		if scheme == "https" && strings.Contains(err.Error(), "bad certificate") {
			return serverResp, fmt.Errorf("The server probably has client authentication (--tlsverify) enabled. Please check your TLS client certification settings: %v", err)
		}
		// Don't decorate context sentinel errors; users may be comparing to
		// them directly.
		switch err {
		case context.Canceled, context.DeadlineExceeded:
			return serverResp, err
		}
		if err, ok := err.(net.Error); ok {
			if err.Timeout() {
				return serverResp, ErrorConnectionFailed(cli.host)
			}
			if !err.Temporary() {
				if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "dial unix") {
					return serverResp, ErrorConnectionFailed(cli.host)
				}
			}
		}
		return serverResp, errors.Wrap(err, "error during connect")
	}
	if resp != nil {
		serverResp.statusCode = resp.StatusCode
	}
	if serverResp.statusCode < 200 || serverResp.statusCode >= 400 {
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return serverResp, err
		}
		if len(body) == 0 {
			return serverResp, fmt.Errorf("Error: request returned %s for API route and version %s, check if the server supports the requested API version", http.StatusText(serverResp.statusCode), req.URL)
		}
		var errorMessage string
		if (cli.version == "" || versions.GreaterThan(cli.version, "1.23")) &&
			resp.Header.Get("Content-Type") == "application/json" {
			var errorResponse types.ErrorResponse
			if err := json.Unmarshal(body, &errorResponse); err != nil {
				return serverResp, fmt.Errorf("Error reading JSON: %v", err)
			}
			errorMessage = errorResponse.Message
		} else {
			errorMessage = string(body)
		}
		return serverResp, fmt.Errorf("Error response from daemon: %s", strings.TrimSpace(errorMessage))
	}
	serverResp.body = resp.Body
	serverResp.header = resp.Header
	return serverResp, nil
}
func (cli *Client) newRequest(method, path string, query url.Values, body io.Reader, headers map[string][]string) (*http.Request, error) {
	apiPath := cli.getAPIPath(path, query)
	req, err := http.NewRequest(method, apiPath, body)
	if err != nil {
		return nil, err
	}
	// Add CLI Config's HTTP Headers BEFORE we set the Docker headers
	// then the user can't change OUR headers
	for k, v := range cli.customHTTPHeaders {
		req.Header.Set(k, v)
	}
	if headers != nil {
		for k, v := range headers {
			req.Header[k] = v
		}
	}
	return req, nil
}

总结

Docker Client创建与命令执行整体逻辑也是比较清楚的。就是定义了一堆命令,然后根据命令行参数,找到cli/command目录下对应的命令执行,而执行逻辑又一般被转至client目录下对应的代码,这里一般都是拼凑一些HTTP请求的URL、参数等,然后使用client/request.go定义的方法向Docker API Server发送请求得到响应,再对响应进行解码得到对象,命令再对得到的对象进行分析处理,最终打印必要的输出。上面我仅分析了docker system info的执行过程,其它命令也很类似。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 golang算法架构leetcode技术php 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Client命令行示例
  • 总结
相关产品与服务
容器镜像服务
容器镜像服务(Tencent Container Registry,TCR)为您提供安全独享、高性能的容器镜像托管分发服务。您可同时在全球多个地域创建独享实例,以实现容器镜像的就近拉取,降低拉取时间,节约带宽成本。TCR 提供细颗粒度的权限管理及访问控制,保障您的数据安全。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档