您的位置:首页 > 运维架构 > Docker

【docker 17 源码分析】docker pull image 源码分析

2016-12-06 11:23 1041 查看

一. Image主要命令



$ docker images (所有)
$ docker images java (所有java)
$ docker images java:8 (固定tag的jave)
$ docker images --no-trunc (所有id值全长度)
$ docker images --digests (所有镜像带有digest)
$ docker images --format "{{.ID}}: {{.Repository}}" (列出两项)
$ docker images --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" (列出三项)
pull主要命令

    Usage:     docker pull [OPTIONS] NAME[:TAG|@DIGEST]

    Pull an image or a repository from a registry

    Options:

      -a, --all-tags                Download all tagged images in the repository

          --disable-content-trust   Skip image verification (default true)

          --help                    Print usage

当执行命令 docker pull,会发生哪些事情?

    本地没有需要的镜像,docker daemon 会去下载需要的 docker 镜像,然后存储在本地;docker 镜像有多个层(layer)组成。最上层(top layer)是可读可写层,用户对镜像的更新在这一层,top layer 之下的层都是只读层;这种实现方式其实也是一种文件系统,UnionFS

二. Image
基础知识


2.1 镜像的子命令

// NewImageCommand returns a cobra command for `image` subcommands
func NewImageCommand(dockerCli *command.DockerCli) *cobra.Command {
cmd := &cobra.Command{
Use:   "image",
Short: "Manage images",
Args:  cli.NoArgs,
RunE:  dockerCli.ShowHelp,
}
cmd.AddCommand(
NewBuildCommand(dockerCli),
NewHistoryCommand(dockerCli),
NewImportCommand(dockerCli),
NewLoadCommand(dockerCli),
NewPullCommand(dockerCli),
NewPushCommand(dockerCli),
NewSaveCommand(dockerCli),
NewTagCommand(dockerCli),
newListCommand(dockerCli),
newRemoveCommand(dockerCli),
newInspectCommand(dockerCli),
NewPruneCommand(dockerCli),
)
return cmd
}

 

拉取镜像的命令: 

    docker pull NAME[:TAG|@DIGEST] ,TAG为标签,DIGEST为数字摘要,也就是拉取镜像可以附带
TAG 或数字摘要等参数,或只使用镜像名(默认latest)。如果参数带
TAG 则使用 NamedTagged 描述 ,如果参数带 DIGEST 则使用Canonical 描述。

$ docker images --digests
$ docker mysql@sha256:89cc6ff6a7ac9916c3384e864fb04b8ee9415b572f872a2a4cf5b909dbbca81b
$ docker pull library/mysql

 

三. 客户端
ImagePull

    
    3.1 ImagePull 函数中 ParseNormalizedNamed 在 3.1.1 节分析,tryImageCreate 在 3.1.2 节分析

func(cli *Client) ImagePull(ctx context.Context, refStr string, options types.ImagePullOptions) (io.ReadCloser, error) {
ref, err := reference.ParseNormalizedNamed(refStr)

query := url.Values{}
query.Set("fromImage", reference.FamiliarName(ref))
if !options.All {
query.Set("tag", getAPITagFromNamedRef(ref))
}

resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth)
if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil {
newAuthHeader, privilegeErr := options.PrivilegeFunc()
resp, err = cli.tryImageCreate(ctx, query, newAuthHeader)
}

return resp.body, nil
}


    3.1.1 ParseNormalizedNamed 中 splitDockerDomain 主要是拆分 repository 名字为 domain 和 remote 名字,没有合法的 domain 指定则使用默认的 docker.io

func ParseNormalizedNamed(s string) (Named, error) {
domain, remainder := splitDockerDomain(s)
var remoteName string
if tagSep := strings.IndexRune(remainder, ':'); tagSep > -1 {
remoteName = remainder[:tagSep]
} else {
remoteName = remainder
}
if strings.ToLower(remoteName) != remoteName {
return nil, errors.New("invalid reference format: repository name must be lowercase")
}

ref, err := Parse(domain + "/" + remainder)
named, isNamed := ref.(Named)
return named, nil
}


    3.1.2 tryImageCreate 将请求 POST 到 daemon 进程

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


四. 服务端 postImagesCreate 
    
    4.1 postImagesCreate 解析请求参数。如果 image 存在则调用 PullImage,空则调用 ImportImage

func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
var (
image   = r.Form.Get("fromImage")
repo    = r.Form.Get("repo")
tag     = r.Form.Get("tag")
)
if image != "" { //pull
......
err = s.backend.PullImage(ctx, image, tag, metaHeaders, authConfig, output)
} else { //import
src := r.Form.Get("fromSrc")
err = s.backend.ImportImage(src, repo, tag, message, r.Body, output, r.Form["changes"])
}

return nil
}

    

五. 服务端 PullImage
    
    5.1 PullImage 中 ParseNormalizedNamed 客户端分析一遍,这里在分析一遍,多多益善!主要函数 pullImageWithReference 5.1.1 节讲解

func (daemon *Daemon) PullImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error {
// Special case: "pull -a" may send an image name with a
// trailing :. This is ugly, but let's not break API
// compatibility.
image = strings.TrimSuffix(image, ":")

ref, err := reference.ParseNormalizedNamed(image)
if err != nil {
return err
}

if tag != "" {
// The "tag" could actually be a digest.
var dgst digest.Digest
dgst, err = digest.Parse(tag)
if err == nil {
ref, err = reference.WithDigest(reference.TrimNamed(ref), dgst)
} else {
ref, err = reference.WithTag(ref, tag)
}
if err != nil {
return err
}
}

return daemon.pullImageWithReference(ctx, ref, metaHeaders, authConfig, outStream)
}


    5.1.1 pullImageWithReference 函数主要调用 distribution.Pull 函数,ImagePullConfig 位于 distribution/pull.go,填充一些配置信息,包括一些接口方法,结构体
5.1.1.1 所示:

unc (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
progressChan := make(chan progress.Progress, 100)
writesDone := make(chan struct{})

ctx, cancelFunc := context.WithCancel(ctx)

go func() {
progressutils.WriteDistributionProgress(cancelFunc, outStream, progressChan)
close(writesDone)
}()

imagePullConfig := &distribution.ImagePullConfig{
Config: distribution.Config{
MetaHeaders:      metaHeaders,
AuthConfig:       authConfig,
ProgressOutput:   progress.ChanOutput(progressChan),
RegistryService:  daemon.RegistryService,
ImageEventLogger: daemon.LogImageEvent,
MetadataStore:    daemon.distributionMetadataStore,
ImageStore:       distribution.NewImageConfigStoreFromStore(daemon.imageStore),
ReferenceStore:   daemon.referenceStore,
},
DownloadManager: daemon.downloadManager,
Schema2Types:    distribution.ImageTypes,
}

err := distribution.Pull(ctx, ref, imagePullConfig)
close(progressChan)
<-writesDone
return err
}


    5.1.1.1 ImagePullConfig 含有 pull 配置

// ImagePullConfig stores pull configuration.
type ImagePullConfig struct {
Config

// DownloadManager manages concurrent pulls.
DownloadManager RootFSDownloadManager
// Schema2Types is the valid schema2 configuration types allowed
// by the pull operation.
Schema2Types []string
}


六. 服务端 distribution Pull

    

    6.1 Pull 函数中,ResolveRepository 函数 6.1.1 节讲解,LookupPullEndpoints 函数可以使用 /etc/docker/daemon.json 中的 endpoint,还有默认的 https://registry-1.docker.io;Pull 使用 v2 版本,路径为
distribution/pull_v2.go 中,6.1.2 节分析

funcPull(ctx context.Context, ref reference.Named, imagePullConfig *ImagePullConfig) error {
// Resolve the Repository name from fqn to RepositoryInfo
repoInfo, err := imagePullConfig.RegistryService.ResolveRepository(ref)

// makes sure name is not `scratch`
if err := ValidateRepoName(repoInfo.Name); err != nil {

endpoints, err := imagePullConfig.RegistryService.LookupPullEndpoints(reference.Domain(repoInfo.Name))

for _, endpoint := range endpoints {
puller, err := newPuller(endpoint, repoInfo, imagePullConfig)

if err := puller.Pull(ctx, ref); err != nil {
}

imagePullConfig.ImageEventLogger(reference.FamiliarString(ref), reference.FamiliarName(repoInfo.Name), "pull")
return nil
}

return TranslatePullError(lastErr, ref)
}


    6.1.1 ResolveRepository 函数拆分reference.Named,配置成RepositoryInfo(描述repository)结构体
6.1.1.1 所示,

// ResolveRepository splits a repository name into its components
// and configuration of the associated registry.
func (s *DefaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
s.mu.Lock()
defer s.mu.Unlock()
return newRepositoryInfo(s.config, name)
}


    6.1.1.1 Repository 结构体如下所示:

// RepositoryInfo describes a repository
type RepositoryInfo struct {
Name reference.Named
// Index points to registry information
Index *registrytypes.IndexInfo
// Official indicates whether the repository is considered official.
// If the registry is official, and the normalized name does not
// contain a '/' (e.g. "foo"), then it is considered an official repo.
Official bool
// Class represents the class of the repository, such as "plugin"
// or "image".
Class string
}


    6.1.2 Pull 函数 NewV2Repository 在 6.1.2.1 节讲解,pullV2Repository 在 6.1.2.2 节讲解

func(p *v2Puller) Pull(ctx context.Context, ref reference.Named) (err error) {
// TODO(tiborvass): was ReceiveTimeout
p.repo, p.confirmedV2, err = NewV2Repository(ctx, p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig, "pull")

if err = p.pullV2Repository(ctx, ref); err != nil {
if continueOnError(err) {
return fallbackError{
err:         err,
confirmedV2: p.confirmedV2,
transportOK: true,
}
}
}
return err
}


   
6.1.2.1 NewV2Repository 函数创建一个提供身份验证的 http 传输通道,并返回 repository 接口,验证 endpoint 可以 ping 通,如下所示:客户端的client中的registry实现了该接口,文件位于vendor/github.com/docker/distribution/registry/client/repository.go

func NewV2Repository(ctx context.Context, repoInfo *registry.RepositoryInfo, endpoint registry.APIEndpoint, metaHeaders http.Header, authConfig *types.AuthConfig, actions ...string) (repo distribution.Repository, foundVersion bool, err error) {
repoName := repoInfo.Name.Name()
// If endpoint does not support CanonicalName, use the RemoteName instead
if endpoint.TrimHostname {
repoName = reference.Path(repoInfo.Name)
}

challengeManager, foundVersion, err := registry.PingV2Registry(endpoint.URL, authTransport)

tr := transport.NewTransport(base, modifiers...)

repoNameRef, err := reference.WithName(repoName)

repo, err = client.NewRepository(ctx, repoNameRef, endpoint.URL.String(), tr)

return
}


    6.1.2.2 pullV2Repository 函数主要根据不是只有名字调用函数 pullV2Tag 于第七章讲解,否则全部版本下载

func (p *v2Puller) pullV2Repository(ctx context.Context, ref reference.Named) (err error) {
var layersDownloaded bool
if !reference.IsNameOnly(ref) {
layersDownloaded, err = p.pullV2Tag(ctx, ref)
} else {
tags, err := p.repo.Tags(ctx).All(ctx)

// The v2 registry knows about this repository, so we will not
// allow fallback to the v1 protocol even if we encounter an
// error later on.
p.confirmedV2 = true

for _, tag := range tags {
tagRef, err := reference.WithTag(ref, tag)

pulledNew, err := p.pullV2Tag(ctx, tagRef)

// pulledNew is true if either new layers were downloaded OR if existing images were newly tagged
// TODO(tiborvass): should we change the name of `layersDownload`? What about message in WriteStatus?
layersDownloaded = layersDownloaded || pulledNew
}
}

writeStatus(reference.FamiliarString(ref), p.config.ProgressOutput, layersDownloaded)

return nil
}


七. 服务端 pullV2Tag 函数

    

    前面都是配置以及验证工作,pullV2Tag 才是真正的最后的下载环节,涉及的内容比较多。

    7.1 Manifests 返回一个 mainfests 结构体于 7.1.1 所示

manSvc, err := p.repo.Manifests(ctx)
if err != nil {
return false, err
}


   7.1.1 mainfests 结构体

type manifests struct {
name   reference.Named
ub     *v2.URLBuilder
client *http.Client
etags  map[string]string
}


    7.2 获取首选项的mainfest服务,这一段就是根据tag或者数字摘要进行 manSvc.Get 方法,以指定的 degest 检索,并返回 distribution.Mainfest 接口

if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
manifest, err = manSvc.Get(ctx, "", distribution.WithTag(tagged.Tag()))
if err != nil {
return false, allowV1Fallback(err)
}
tagOrDigest = tagged.Tag()
} else if digested, isDigested := ref.(reference.Canonical); isDigested {
manifest, err = manSvc.Get(ctx, digested.Digest())
if err != nil {
return false, err
}
tagOrDigest = digested.Digest().String()
} else {
return false, fmt.Errorf("internal error: reference has neither a tag nor a digest: %s", reference.FamiliarString(ref))
}


    7.3 pullV2Tag 主要调用 pullSchema2 于 第八章讲解

switch v := manifest.(type) {
case *schema1.SignedManifest:
if p.config.RequireSchema2 {
return false, fmt.Errorf("invalid manifest: not schema2")
}
id, manifestDigest, err = p.pullSchema1(ctx, ref, v)
if err != nil {
return false, err
}
case *schema2.DeserializedManifest:
id, manifestDigest, err = p.pullSchema2(ctx, ref, v)
if err != nil {
return false, err
}
case *manifestlist.DeserializedManifestList:
id, manifestDigest, err = p.pullManifestList(ctx, ref, v)
if err != nil {
return false, err
}
default:
return false, errors.New("unsupported manifest format")
}


八. 服务端 pullSchema2 函数

    

    8.1 schema2ManifestDigest 函数获得 digest

manifestDigest, err = schema2ManifestDigest(ref, mfst)
if err != nil {
return "", "", err
}


    8.2 Get 方法cont /var/lib/docker/image/aufs/imagedb/content/sha256/${image-id},查询镜像配置,如果 digest 已经存在直接返回,大致如下所示

if _, err := p.config.ImageStore.Get(target.Digest); err == nil {
// If the image already exists locally, no need to pull
// anything.
return target.Digest, manifestDigest, nil
}


{

    "architecture": "amd64",

    "config": {

        "Hostname": "44c72a15738e",

        "Domainname": "",

        "User": "",

        "AttachStdin": false,

        "AttachStdout": false,

        "AttachStderr": false,

        "Tty": false,

        "OpenStdin": false,

        "StdinOnce": false,

        "Env": [

            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

        ],

        "Cmd": [

            "sh"

        ],

        "ArgsEscaped": true,

        "Image": "sha256:ed8808c239d47dbafc07d08ab8cd4a00cc1f6960f3a3899038af39beea060d3a",

        "Volumes": null,

        "WorkingDir": "",

        "Entrypoint": null,

        "OnBuild": null,

        "Labels": {}

    },

    "container": "023595dd42103f71440c07e0871678156d62bf28428cbd7685690ae838191f62",

    "container_config": {

        "Hostname": "44c72a15738e",

        "Domainname": "",

        "User": "",

        "AttachStdin": false,

        "AttachStdout": false,

        "AttachStderr": false,

        "Tty": false,

        "OpenStdin": false,

        "StdinOnce": false,

        "Env": [

            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

        ],

        "Cmd": [

            "/bin/sh",

            "-c",

            "#(nop) ",

            "CMD [\"sh\"]"

        ],

        "ArgsEscaped": true,

        "Image": "sha256:ed8808c239d47dbafc07d08ab8cd4a00cc1f6960f3a3899038af39beea060d3a",

        "Volumes": null,

        "WorkingDir": "",

        "Entrypoint": null,

        "OnBuild": null,

        "Labels": {}

    },

    "created": "2017-07-19T23:34:19.030879144Z",

    "docker_version": "17.03.1-ce",

    "history": [

        {

            "created": "2017-07-19T23:34:11.72766006Z",

            "created_by": "/bin/sh -c #(nop) ADD file:0516fc7a5988ef4bc7b691588095c80f3ea8637eb37141cfe5f6cb859d6955c8 in / "

        },

        {

            "created": "2017-07-19T23:34:19.030879144Z",

            "created_by": "/bin/sh -c #(nop)  CMD [\"sh\"]",

            "empty_layer": true

        }

    ],

    "os": "linux",

    "rootfs": {

        "type": "layers",

        "diff_ids": [

            "sha256:08c2295a7fa5c220b0f60c994362d290429ad92f6e0235509db91582809442f3"

        ]

    }

}

    8.3 下载配置文件并写入 /var/lib/docker/image/aufs/imagedb/content/sha256/${image-id}

// Pull the image config
go func() {
configJSON, err := p.pullSchema2Config(ctx, target.Digest)
if err != nil {
configErrChan <- ImageConfigPullError{Err: err}
cancel()
return
}
configChan <- configJSON
}()


    8.4 下载个层 tar 包镜像文件

for _, d := range mfst.Layers {
layerDescriptor := &v2LayerDescriptor{
digest:            d.Digest,
repo:              p.repo,
repoInfo:          p.repoInfo,
V2MetadataService: p.V2MetadataService,
src:               d,
}

descriptors = append(descriptors, layerDescriptor)
}

if p.config.DownloadManager != nil {
go func() {
var (
err    error
rootFS image.RootFS
)
downloadRootFS := *image.NewRootFS()
rootFS, release, err = p.config.DownloadManager.Download(ctx, downloadRootFS, descriptors, p.config.ProgressOutput)

downloadedRootFS = &rootFS
close(downloadsDone)
}()
}


总结:

将 repository 名字分析成远端域 + 名字
由镜像名请求Manifest Schema v2
解析Manifest获取镜像Configuration
下载各 Layer gzip 压缩文件
验证Configuration中的RootFS.DiffIDs是否与下载(解压后)hash相同
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: