docker命令行解析以及如何向服务器端发送请求(docker源码学习一)

时间:2021-01-04 20:37:14

最近在看doccker的源码,最新的master分支(估计是1.12.4,因为最新的release是1.12.3)命令行解析全部都使用了第3方的包https://github.com/spf13/cobra。然后看了一下别的分支的代码,感觉结构确实清晰了很多,可读性变高了不少。先看一下如何去使用。

客户端main()在docker/docker/cmd/docker下,可以直接使用go build编译(把vendor下的依赖包移出来就可以了)。

L20-58:

func newDockerCommand(dockerCli *command.DockerCli) *cobra.Command {
	opts := cliflags.NewClientOptions()
	var flags *pflag.FlagSet

	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)

	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)

	cmd.SetOutput(dockerCli.Out())
	cmd.AddCommand(newDaemonCommand())
	commands.AddCommands(cmd, dockerCli)

	return cmd
}

这个方法会完成所有命令行规则的添加,和相应执行的方法。RunE: func(cmd *cobra.Command, args []string) error {}是命令执行的方法,flags是参数的解析。当我们输入docker -v,执行showversion()方法,*cobra.Command结构体是一个树的结构,docker下面有很多的子命令,比如docker image,然后image下面可以添加参数 -a等,最后只要在main()中执行cmd.Execute()就可以完成所有解析,这是也是第3方包的方法。

ctrl+鼠标右键点击进入commands.AddCommands(cmd, dockerCli)

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),
。。。)
}

封装了一下cmd.AddCommand()方法,然后node.service.stack.swarm.container这些包都写了个大写new方法来创建命令,继续进入container.NewContainerCommand(dockerCli),这是所有的方法都传入了dockerCli(单例模式直视感),

func NewContainerCommand(dockerCli *command.DockerCli) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "container",
		Short: "Manage containers",
		Args:  cli.NoArgs,
		Run: func(cmd *cobra.Command, args []string) {
			fmt.Fprintf(dockerCli.Err(), "\n"+cmd.UsageString())
		},
	}
	cmd.AddCommand(
		NewAttachCommand(dockerCli),
		NewCommitCommand(dockerCli),
		NewCopyCommand(dockerCli),
		NewCreateCommand(dockerCli),
		NewDiffCommand(dockerCli),
		NewExecCommand(dockerCli),
		NewExportCommand(dockerCli),
		NewKillCommand(dockerCli),
		NewLogsCommand(dockerCli),
		NewPauseCommand(dockerCli),
		NewPortCommand(dockerCli),
		NewRenameCommand(dockerCli),
		NewRestartCommand(dockerCli),
		NewRmCommand(dockerCli),
		NewRunCommand(dockerCli),
		NewStartCommand(dockerCli),
		NewStatsCommand(dockerCli),
		NewStopCommand(dockerCli),
		NewTopCommand(dockerCli),
		NewUnpauseCommand(dockerCli),
		NewUpdateCommand(dockerCli),
		NewWaitCommand(dockerCli),
		newListCommand(dockerCli),
		newInspectCommand(dockerCli),
		NewPruneCommand(dockerCli),
	)
	return cmd
}

  docker contain下又添加了很多的命令,继续进入

func NewCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
	var opts createOptions
	var copts *runconfigopts.ContainerOptions

	cmd := &cobra.Command{
		Use:   "create [OPTIONS] IMAGE [COMMAND] [ARG...]",
		Short: "Create a new container",
		Args:  cli.RequiresMinArgs(1),
		RunE: func(cmd *cobra.Command, args []string) error {
			copts.Image = args[0]
			if len(args) > 1 {
				copts.Args = args[1:]
			}
			return runCreate(dockerCli, cmd.Flags(), &opts, copts)
		},
	}

	flags := cmd.Flags()
	flags.SetInterspersed(false)

	flags.StringVar(&opts.name, "name", "", "Assign a name to the container")

	// Add an explicit help that doesn't have a `-h` to prevent the conflict
	// with hostname
	flags.Bool("help", false, "Print usage")

	command.AddTrustedFlags(flags, true)
	copts = runconfigopts.AddFlags(flags)
	return cmd
}

  这里可以看到解析了name和help,docker run create命令就是这么来的,然后看一下RunE中是如何向服务器端发送请求的,继续进入最后可以得到一个这个方法,response, err := dockerCli.Client().ContainerCreate(ctx, config, hostConfig, networkingConfig, name)

func (cli *Client) ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, containerName string) (types.ContainerCreateResponse, error) {
	var response types.ContainerCreateResponse
	query := url.Values{}
	if containerName != "" {
		query.Set("name", containerName)
	}

	body := configWrapper{
		Config:           config,
		HostConfig:       hostConfig,
		NetworkingConfig: networkingConfig,
	}

	serverResp, err := cli.post(ctx, "/containers/create", query, body, nil)
	if err != nil {
		if serverResp != nil && serverResp.statusCode == 404 && strings.Contains(err.Error(), "No such image") {
			return response, imageNotFoundError{config.Image}
		}
		return response, err
	}

	err = json.NewDecoder(serverResp.body).Decode(&response)
	ensureReaderClosed(serverResp)
	return response, err
}

发送post /containers/create请求,细节的地方需要继续去看,服务器端的命令解析也是一样的,目录在docker/docker/cmd/dockerd下,go编译的包是通过包名获取的,所以deamon的命令就是dockerd,有一个很好的方法是dockerd -D,可以看到调试的信息,service api的添加,网络的初始化等。。

https://github.com/zjj2wry/httptest,这是拿那个命令行解析写的一个post请求,可以携带参数,cookies等,然后生成接口文档,对我已经完全够了(目前业务百分之90都是post),主要写个接口得给前端一个文档,偷个懒。。