【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相同
相关文章推荐
- 【docker 17 源码分析】 Docker Client源码分析
- 【docker 17 源码分析】 docker run container 源码分析二 docker start
- 【docker 17 源码分析】docker run container 源码分析一 docker create
- 【docker 17 源码分析】 Docker Daemon启动
- Docker源码分析(一):Docker架构
- 谷歌浏览器的源码分析(17)
- Cocos2d-x学习笔记(17)(TestCpp源码分析-1)
- g723源码详细分析-17-舒适噪声解码
- Docker源码分析(一):Docker架构
- Docker源码分析之——Docker Client的启动与命令执行
- [原创] jQuery源码分析-17坐标和尺寸-Offset&Dimensions
- jQuery源码分析-17尺寸和大小 Dimensions & Offset
- effective c++条款13-17 “以对象管理资源”之auto_ptr源码分析
- Docker源码分析(一):Docker架构
- Docker源码分析:Docker架构
- Docker源码分析(二):Docker Client创建与命令执行
- 深入学习Django源码基础17 - django中messagee分析学习
- [原创] jQuery源码分析-17坐标和尺寸 Offset & Dimensions
- Spring Security3源码分析(17)-Filter链排序分析
- Docker源码分析(三):Docker Daemon启动