docker pull命令实现与镜像存储(2)

在《pull命令实现与镜像存储(1)》我们了解到拉取镜像时,runPull函数通过reference.ParseNamed(opts.remote)得到一个描述镜像信息的Named对象。以命令docker pull ubuntu:latest为例,将得到一个NamedTagged对象。我们再看下reference.ParseNamed(image):

// ParseNamed parses s and returns a syntactically valid reference implementing
// the Named interface. The reference must have a name, otherwise an error is
// returned.
// If an error was encountered it is returned, along with a nil Reference.
func ParseNamed(s string) (Named, error) {
    named, err := distreference.ParseNamed(s)
    if err != nil {
        return nil, fmt.Errorf("Error parsing reference: %q is not a valid repository/tag: %s", s, err)
    }
    // If no valid hostname is found, the default hostname is used./如果没有有效的主机名,则使用默认的主机名docker.io
    //将distreference.Namded转化为reference.Named
    r, err := WithName(named.Name())
    if err != nil {
        return nil, err
    }
    if canonical, isCanonical := named.(distreference.Canonical); isCanonical {
        //将distreference.Canonical转化为reference.Canonical
        return WithDigest(r, canonical.Digest())
    }
    if tagged, isTagged := named.(distreference.NamedTagged); isTagged {
        //将distreference.NamedTagged转化为reference.NamedTagged
        return WithTag(r, tagged.Tag())
    }
    return r, nil
}

可以看到,如果我们传入的参数是tag形式,那么将执行distreference.NamedTagged转化为reference.NamedTagged。我们知道reference.NamedTagged只是Named的子接口,是不能直接使用的,一定要有一个实现类。我们进WithTag函数看下,看下reference.NamedTagged的实现形式:

// WithTag combines the name from "name" and the tag from "tag" to form a
// reference incorporating both the name and the tag.
func WithTag(name Named, tag string) (NamedTagged, error) {
    r, err := distreference.WithTag(name, tag)
    if err != nil {
        return nil, err
    }
    //reference.NamedTagged接口的实现类
    return &taggedRef{namedRef{r}}, nil
}

可以看到返回的是taggedRef对象,即是NamedTagged的实现类,我们看下该类:

type namedRef struct {
    distreference.Named
}
type taggedRef struct {
    namedRef
}

可以看到taggedRef 实际上包含了一个distreference.Named,有go语言的特性知,taggedRef 将继承distreference.Named的所有方法,即是taggedRef 拥有distreference.taggedReference的所有方法(distreference.NamedTagged是distreference.Named的子接口,distreference.taggedReference是distreference.NamedTagged的实现类)。
好了,现在我们得到一个taggedRef镜像的描述对象,我们回到runPull函数:

func runPull(dockerCli *command.DockerCli, opts pullOptions) error {
    //从参数中解析出带镜像仓库地址等信息的镜像引用,如果参数中没有仓库地址信息,则使用默认的docker.io
    distributionRef, err := reference.ParseNamed(opts.remote)
    if err != nil {
        return err
    }
......
 //加了禁止镜像校验disable-content-trust,而且不带数字签名的tag
    if command.IsTrusted() && !registryRef.HasDigest() {
        // Check if tag is digest
        err = trustedPull(ctx, dockerCli, repoInfo, registryRef, authConfig, requestPrivilege)
    } else {
        //向dockerd发送拉取镜像请求
        err = imagePullPrivileged(ctx, dockerCli, authConfig, distributionRef.String(), requestPrivilege, opts.all)
    }
    if err != nil {
        if strings.Contains(err.Error(), "target is a plugin") {
            return errors.New(err.Error() + " - Use `docker plugin install`")
        }
        return err
    }

    return nil
}

runPull对该对象进行一些检查,我们不关心;如果加了disable-content-trust选项,还会做其他一些事情才继续往下,我们先不管。我们直接看到:

err = imagePullPrivileged(ctx, dockerCli, authConfig, distributionRef.String(), requestPrivilege, opts.all)

可以看到我们得到taggedRef对象distributionRef,字符串化之后,作为imagePullPrivileged函数的参数,distributionRef的String函数来自于distreference.taggedReference:

func (t taggedReference) String() string {
    return t.name + ":" + t.tag
}

可以发现,都已了一大圈,还是我们传入时的形式,即如例:ubunu:latest。不知基于何考虑,我们先不去深究,我们继续分析函imagePullPrivileged:

// imagePullPrivileged pulls the image and displays it to the output
func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error {
    //在docker官网注册的账户信息
    encodedAuth, err := command.EncodeAuthToBase64(authConfig)
    if err != nil {
        return err
    }
    options := types.ImagePullOptions{
        RegistryAuth:  encodedAuth,
        PrivilegeFunc: requestPrivilege,
        All:           all,
    }
        //实现在docker\client\image_pull.go
    responseBody, err := cli.Client().ImagePull(ctx, ref, options)
    if err != nil {
        return err
    }
    defer responseBody.Close()

    return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil)
}

我们之前了解到docker实现为一个C/S架构,docker作为客户端接受用户命令,解析完之后,将向dockerd发送请求,然后由dockerd完成用户的请求,下面的语句就是docker向dockerd发送请求:

responseBody, err := cli.Client().ImagePull(ctx, ref, options)

我们看下cli.Client():

// Client returns the APIClient
func (cli *DockerCli) Client() client.APIClient {
    return cli.client
}

那这个cli.client是什么呢?为了连贯性,我们先不分析这个client怎么来的,我们直接给出client的image_pull
实现代码在文件docker\client\image_pull.go,我们看下:

func (cli *Client) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
    //这里又解析了一遍,搞不懂为何这么绕来绕去,
    repository, tag, err := reference.Parse(ref)
    if err != nil {
        return nil, err
    }

    query := url.Values{}
    query.Set("fromImage", repository)
    if tag != "" && !options.All {
        query.Set("tag", tag)
    }
       //发送http请求到dockerd
    resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth)
    if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
        newAuthHeader, privilegeErr := options.PrivilegeFunc()
        if privilegeErr != nil {
            return nil, privilegeErr
        }
        resp, err = cli.tryImageCreate(ctx, query, newAuthHeader)
    }
    if err != nil {
        return nil, err
    }
    return resp.body, nil
}

可以看到,ImagePull函数又把我们传入的参数解析了一遍,而且使用同样的方法,解析出镜像名和tag,作为http的header,发送到dockerd(真实无语了),这样在dockerd将接收到镜像名和tag等结构化参数。我们看下这些参数将发送到那里:


func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) {
    headers := map[string][]string{"X-Registry-Auth": {registryAuth}}
    return cli.post(ctx, "/images/create", query, nil, headers)
}

可以看到将发送到服务端的这个URL:/images/create。至此客户端这边的工作也先告一段落。后面我们将分析dockerd关于拉取镜像的代码。

最后,我们讲下client是怎么来的。docker首先执行main函数:

func main() {
    // Set terminal emulation based on platform as required.
    stdin, stdout, stderr := term.StdStreams()
    logrus.SetOutput(stderr)

    dockerCli := command.NewDockerCli(stdin, stdout, stderr)
    //主命令
    cmd := newDockerCommand(dockerCli)
       //执行命令
    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函数,初始化一个DockerCli对象。client是作为cli的成员出现的,所以我们留意下cli是怎么初始化的。先看NewDockerCli:

// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
func NewDockerCli(in io.ReadCloser, out, err io.Writer) *DockerCli {
    return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err}
}

很简单,传入输入输出对象,初始化一个cli
接着 cmd := newDockerCommand(dockerCli):

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
        },
        //在run之前执行
        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)
            //初始化一个client
            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())
    //添加daemon选项,其实已经作废了,应为新代码已经将客户端和服务端守护进程命令分开了,不需要这个选项区分启动客户端还是服务端
    cmd.AddCommand(newDaemonCommand())
    // AddCommands adds all the commands from cli/command to the root command
    //添加子命令及子命令的选项
    commands.AddCommands(cmd, dockerCli)

    return cmd
}

看到:

        //在run之前执行
        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)
            //初始化一个client
            return dockerCli.Initialize(opts)
        },

这部分代码将在命令执行之前被执行,我们看到有dockerCli.Initialize(opts):

// Initialize the dockerCli runs initialization that must happen after command
// line flags are parsed.
func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions) error {
    cli.configFile = LoadDefaultConfigFile(cli.err)

    var err error
    //生成client对象
    cli.client, err = NewAPIClientFromFlags(opts.Common, cli.configFile)
    if err != nil {
        return err
    }
    if opts.Common.TrustKey == "" {
        cli.keyFile = filepath.Join(cliconfig.ConfigDir(), cliflags.DefaultTrustKeyFile)
    } else {
        cli.keyFile = opts.Common.TrustKey
    }

    return nil
}

可以看到cli的client就是在这里被赋值的,进入client就可以找到client中实现了我们所有的命令执行函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值