我们在《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)
两相对比,可以发现两个文件都定义了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函数使用
转了那么多圈,也就干了这么点事情。