我们在《docker命令解析》篇章我们了解了命令的解析过程,所以不再赘述。我们直接看执行命令任务的代码。
定位到docker\cli\command\commands\commands.go的AddCommands函数,我们容易找到pull命令的实现函数 在hide(image.NewPullCommand(dockerCli))注册。我们进入该函数:

// NewPullCommand creates a new `docker pull` command
func NewPullCommand(dockerCli *command.DockerCli) *cobra.Command {
    var opts pullOptions

    cmd := &cobra.Command{
        Use:   "pull [OPTIONS] NAME[:TAG|@DIGEST]",
        Short: "Pull an image or a repository from a registry",
        Args:  cli.ExactArgs(1),
        RunE: func(cmd *cobra.Command, args []string) error {
            //镜像名字,如:docker pull ubuntu,则args[0]就是ubuntu
            opts.remote = args[0]
            return runPull(dockerCli, opts)
        },
    }

    flags := cmd.Flags()

    flags.BoolVarP(&opts.all, "all-tags", "a", false, "Download all tagged images in the repository")
    command.AddTrustedFlags(flags, true)

    return cmd
}

我们了解了命令的解析过程,容易知道将执行函数runPull,同时将拉取的镜像参数传入(镜像名,版本,是否所有tag等),我们看下函数runPull:

func runPull(dockerCli *command.DockerCli, opts pullOptions) error {
    //从参数中解析出带镜像仓库地址等信息的镜像引用,如果参数中没有仓库地址信息,则使用默认的docker.io
    distributionRef, err := reference.ParseNamed(opts.remote)
    if err != nil {
        return err
    }
    // -a, --all-tags                Download all tagged images in the repository
    //如果使用了all选项,但是又不是只有镜像名(包含tag),则报错处理
    if opts.all && !reference.IsNameOnly(distributionRef) {
        return errors.New("tag can't be used with --all-tags/-a")
    }
    //如果没有使用all选项,且只有镜像名,则添加一个默认的tag(latest)
    if !opts.all && reference.IsNameOnly(distributionRef) {
        distributionRef = reference.WithDefaultTag(distributionRef)
        fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", reference.DefaultTag)
    }

    var tag string
    switch x := distributionRef.(type) {
    //标准的
    case reference.Canonical:
        tag = x.Digest().String()
    //name:tag形式
    case reference.NamedTagged:
        tag = x.Tag()
    }

    registryRef := registry.ParseReference(tag)

    // Resolve the Repository name from fqn to RepositoryInfo
    repoInfo, err := registry.ParseRepositoryInfo(distributionRef)
    if err != nil {
        return err
    }

    ctx := context.Background()

    authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
    requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "pull")
        //如果没有添加disable-content-trust,而且没有附带数字摘要,则不对镜像进行校验
    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
}

该函数做了两件事:解析输入参数填充Named结构对象,用字符串化的Named对象拉取镜像。在详细说明之前,我们有必要讲一下Named这个接口。

// Named is an object with a full name
type Named interface {
    // Name returns normalized repository name, like "ubuntu".
    Name() string
    // String returns full reference, like "ubuntu@sha256:abcdef..."
    String() string
    // FullName returns full repository name with hostname, like "docker.io/library/ubuntu"
    FullName() string
    // Hostname returns hostname for the reference, like "docker.io"
    Hostname() string
    // RemoteName returns the repository component of the full name, like "library/ubuntu"
    RemoteName() string
}

Named接口有两个子接口带数字摘要的Canonical和带tag的NamedTagged:

//带数字摘要的形式
// Canonical reference is an object with a fully unique
// name including a name with hostname and digest
type Canonical interface {
    Named
    Digest() digest.Digest
}
//带tag的形式
// NamedTagged is an object including a name and tag.
type NamedTagged interface {
    Named
    Tag() string
}

我们知道拉取镜像的命令:docker pull NAME[:TAG|@DIGEST] ,TAG代表标签,DIGEST代表数字摘要,意思就是我们拉取镜像参数可以附带TAG或数字摘要,或者只带镜像名(系统会提供一个默认的标签latest)。如果我们提供的参数带TAG则使用NamedTagged描述 ,如果我们提供的参数带DIGEST则使用Canonical 描述。现在我们简单分析下这个解析过程,函数调用过程:
reference.ParseNamed(opts.remote)–>distreference.ParseNamed(s)–> Parse(s)
函数reference.ParseNamed(opts.remote)实现在docker\reference\reference.go

函数distreference.ParseNamed(s)和函数Parse(s)都是定义在文件docker\vendor\src\github.com\docker\distribution\reference\reference.go
可以发现文件名都为reference.go,感觉起来就有点蹊跷,事实上感觉是对的。我们看下两个文件的结构(上面是docker\reference\reference.go)

docker\reference\reference.go

docker\vendor\src\github.com\docker\distribution\reference\reference.go
两相对比,可以发现两个文件都定义了Named,Canonical,NamedTagged三个接口,而且接口间的关系也是一样的。实际上参数的正则匹配是在后者的Parse函数完成,一切做好之后,才在前者的reference.ParseNamed(opts.remote)函数中做一个转化(暂时还不了解为何要这样写代码),看reference.ParseNamed(opts.remote)函数:

// 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
}

reference.ParseNamed(opts.remote)函数不过是个马甲,实际工作并不是自己做的,看下完成正则匹配的Parse函数:

// Parse parses s and returns a syntactically valid Reference.
// If an error was encountered it is returned, along with a nil Reference.
// NOTE: Parse will not handle short digests.
func Parse(s string) (Reference, error) {
    //
    //
    matches := ReferenceRegexp.FindStringSubmatch(s)
    if matches == nil {
        if s == "" {
            return nil, ErrNameEmpty
        }
        // TODO(dmcgowan): Provide more specific and helpful error
        return nil, ErrReferenceInvalidFormat
    }

    if len(matches[1]) > NameTotalLengthMax {
        return nil, ErrNameTooLong
    }

    ref := reference{
        name: matches[1],
        tag:  matches[2],
    }
    //带数字摘要,有SHA256, SHA384, SHA512,一般为SHA256
    if matches[3] != "" {
        var err error
        //主要是校验
        ref.digest, err = digest.ParseDigest(matches[3])
        if err != nil {
            return nil, err
        }
    }
    //这里根据解析参数是否包含镜像名,是否包含标签,以及是否包含数字摘要来决定返回引用的类型
    r := getBestReferenceType(ref)
    if r == nil {
        return nil, ErrNameEmpty
    }

    return r, nil
}

ReferenceRegexp匹配规则定义docker\vendor\src\github.com\docker\distribution\reference\regexp.go

    ReferenceRegexp = anchored(capture(NameRegexp),
        optional(literal(":"), capture(TagRegexp)),
        optional(literal("@"), capture(DigestRegexp)))

可以看到跟我们的命令的形式是对应的,如果带镜像名,则matches[1]不为空,如果带tag,则matches[2]不为空,如果带数字摘要,则matches[3]不为空。getBestReferenceType根据各个matchs是否为空,返回对应的引用Reference。我们接着分析下函数getBestReferenceType:

func getBestReferenceType(ref reference) Reference {
    //只带数字摘要
    if ref.name == "" {
        // Allow digest only references
        if ref.digest != "" {
            return digestReference(ref.digest)
        }
        return nil
    }
    //带数字摘要和镜像名
    if ref.tag == "" {
        if ref.digest != "" {
            return canonicalReference{
                name:   ref.name,
                digest: ref.digest,
            }
        }
        return repository(ref.name)
    }
    //带标签和镜像名
    if ref.digest == "" {
        return taggedReference{
            name: ref.name,
            tag:  ref.tag,
        }
    }

    return ref
}

函数逻辑很简单,就是根据是否带相应的部分返回不同类型的Reference 。
好了,我们把上面的过程梳理下:
第一,我们传入拉取镜像的参数,如我们执行docker pull ubuntu:latest,则“ubuntu:latest”将被Parse解析为三个部分matches[1]=ubuntu,matches[2]=latest,matches[3]=”“,并返回NamedTagged类型的Reference对象(distreference.Named为Reference的子接口,也即是返回distreference.Named对象)

第二,reference.ParseNamed(opts.remote)将Reference(distreference.Named)对象转化为reference.Named对象
第三,reference.ParseNamed(opts.remote)返回reference.Named给pull函数使用
转了那么多圈,也就干了这么点事情。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐