我们在《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 pullOptionscmd := &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]就是ubuntuopts.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.iodistributionRef, 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 stringswitch 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 RepositoryInforepoInfo, 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 digesterr = 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 {NamedDigest() digest.Digest
}
//带tag的形式
// NamedTagged is an object including a name and tag.
type NamedTagged interface {NamedTag() 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)
两相对比,可以发现两个文件都定义了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.Namedr, err := WithName(named.Name())if err != nil {return nil, err}if canonical, isCanonical := named.(distreference.Canonical); isCanonical {//将distreference.Canonical转化为reference.Canonicalreturn WithDigest(r, canonical.Digest())}if tagged, isTagged := named.(distreference.NamedTagged); isTagged {//将distreference.NamedTagged转化为reference.NamedTaggedreturn 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 errorreturn nil, ErrReferenceInvalidFormat}if len(matches[1]) > NameTotalLengthMax {return nil, ErrNameTooLong}ref := reference{name: matches[1],tag: matches[2],}//带数字摘要,有SHA256, SHA384, SHA512,一般为SHA256if 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 referencesif 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函数使用
转了那么多圈,也就干了这么点事情。