• docker push 实现过程


    这一篇文章分析一下docker push的过程;docker push是将本地的镜像上传到registry service的过程;

    根据前几篇文章,可以知道客户端的命令是在api/client/push.go中,CmdPush()函数:

    基本思路就是将通过解析cmd.Arg(0)参数,提取去要push的镜像的repository 和 tag,通过registry 和 repository获得repostoryInfo;如果需要安全验证,那么还要设置一下authConfig;接着通过POST:/images/xxxx/push? 请求调用server端的postImagesPush()函数;(在api/server/image.go)中,主要来分析一下这个函数:

    func (s *Server) postImagesPush(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string)   error {

        if vars == nil {

            return fmt.Errorf("Missing parameter")

        } 

        metaHeaders := map[string][]string{}

        for k, v := range r.Header {

            if strings.HasPrefix(k, "  ") {

                metaHeaders[k] = v

            }

        }

        if err := parseForm(r); err != nil {

            return err

        }

        authConfig := &cliconfig.AuthConfig{}

        authEncoded := r.Header.Get("X-Registry-Auth")

        if authEncoded != "" {

            // the new format is to handle the authConfig as a header

            authJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))

            if err := json.NewDecoder(authJSON).Decode(authConfig); err != nil {

                // to increase compatibility to existing api it is defaulting to be empty

                authConfig = &cliconfig.AuthConfig{}

            }

        } else {

            // the old format is supported for compatibility if there was no authConfig header 

            if err := json.NewDecoder(r.Body).Decode(authConfig); err != nil {

                return fmt.Errorf("Bad parameters and missing X-Registry-Auth: %v", err)

            }

        }

        name := vars["name"]

        output := ioutils.NewWriteFlusher(w)

        imagePushConfig := &graph.ImagePushConfig{

            MetaHeaders: metaHeaders,

            AuthConfig:  authConfig,

            Tag:         r.Form.Get("tag"),

            OutStream:   output,

        }

        w.Header().Set("Content-Type", "application/json")

        if err := s.daemon.Repositories().Push(name, imagePushConfig); err != nil {

            if !output.Flushed() {

                return err

            }

            sf := streamformatter.NewJSONStreamFormatter()

            output.Write(sf.FormatError(err))

        }

        return nil

    }

    最主要的流程是前面是封装好 imagePushConfig 这个数据结构,imagePushConfig主要包括http request中的header信息、镜像tag、认证信息以及一个回写客户端的output Writer(),然后调用s.daemon.Repositories().Push(name, imagePushConfig) 来进行镜像文件的上传;之前的文章中介绍过,deamon的Repositories()函数返回的其实是TagStore结构(/graph/tags.go 文件中);直接去看一下TagStore()的Push函数:

    // Push initiates a push operation on the repository named localName.

    func (s *TagStore) Push(localName string, imagePushConfig *ImagePushConfig) error {

        // FIXME: Allow to interrupt current push when new push of same image is done.

        var sf = streamformatter.NewJSONStreamFormatter()

        // Resolve the Repository name from fqn to RepositoryInfo

        repoInfo, err := s.registryService.ResolveRepository(localName)

        if err != nil {

            return err

        }

        endpoints, err := s.registryService.LookupPushEndpoints(repoInfo.CanonicalName)

        if err != nil {

            return err

        }

        reposLen := 1

        if imagePushConfig.Tag == "" {

            reposLen = len(s.Repositories[repoInfo.LocalName])

        }

        imagePushConfig.OutStream.Write(sf.FormatStatus("", "The push refers to a repository [%s] (len: %d)", repoInfo.        CanonicalName, reposLen))

        // If it fails, try to get the repository

        localRepo, exists := s.Repositories[repoInfo.LocalName]

        if !exists {

            return fmt.Errorf("Repository does not exist: %s", repoInfo.LocalName)

        } 

        var lastErr error

        for _, endpoint := range endpoints {

            logrus.Debugf("Trying to push %s to %s %s", repoInfo.CanonicalName, endpoint.URL, endpoint.Version)

            pusher, err := s.NewPusher(endpoint, localRepo, repoInfo, imagePushConfig, sf)

            if err != nil {

                lastErr = err

                continue

            }

            if fallback, err := pusher.Push(); err != nil {

                if fallback {

                    lastErr = err

                    continue

                }

                logrus.Debugf("Not continuing with error: %v", err)

                return err

            }

            s.eventsService.Log("push", repoInfo.LocalName, "")

            return nil

        }

        if lastErr == nil {

            lastErr = fmt.Errorf("no endpoints found for %s", repoInfo.CanonicalName)

        }

        return lastErr

    }

    首先通过localName获取repositoryInfo,然后通过repositoryInfo获取endpoints,然后针对每一个endpoint,实例化一个pusher,通过pusher.Push()来实现镜像的上传,程序的流程和结构很好理解;看一下NewPusher()的代码:

    func (s *TagStore) NewPusher(endpoint registry.APIEndpoint, localRepo Repository, repoInfo *registry.RepositoryInfo,       imagePushConfig *ImagePushConfig, sf *streamformatter.StreamFormatter) (Pusher, error) {

        switch endpoint.Version {

        case registry.APIVersion2:

            return &v2Pusher{

                TagStore:  s,

                endpoint:  endpoint,

                localRepo: localRepo,

                repoInfo:  repoInfo,

                config:    imagePushConfig,

                sf:        sf,

            }, nil

        case registry.APIVersion1:

            return &v1Pusher{

                TagStore:  s,

                endpoint:  endpoint,

                localRepo: localRepo,

                repoInfo:  repoInfo,

                config:    imagePushConfig,

                sf:        sf,

            }, nil

        }

        return nil, fmt.Errorf("unknown version %d for registry %s", endpoint.Version, endpoint.URL)

    }

    主要是根据endpoint中version的不同,来决定实例化哪个pushser;暂且以v2 pusher()为例来进行分析;

    type v2Pusher struct {

        *TagStore

        endpoint  registry.APIEndpoint

        localRepo Repository

        repoInfo  *registry.RepositoryInfo

        config    *ImagePushConfig

        sf        *streamformatter.StreamFormatter

        repo      distribution.Repository

    }

    这个是v2Pusher的定义;接下来是v2Pusher的Push(/graph/push_v2.go文件)函数:

    func (p *v2Pusher) Push() (fallback bool, err error) {

        p.repo, err = NewV2Repository(p.repoInfo, p.endpoint, p.config.MetaHeaders, p.config.AuthConfig)

        if err != nil {

            logrus.Debugf("Error getting v2 registry: %v", err)

            return true, err

        }

        return false, p.pushV2Repository(p.config.Tag)

    }

    首先实例化出来NewV2Repository实例(/graph/registry.go文件中)复制给pusher的repo实例,然后调用pusher的pushV2Repository函数进行镜像的上传;新的pusher.repo实例,也就是NewV2Repository返回的其实是一个新的repository(vendor/src/github.com/docker/distribution/registry/client/repository.go文件中),它主要新建了一个提供了超时设定、权限验证和对远程endpoint的api version进行验证的http transport,负责镜像的上传;接着看一下最重要的pusher的pushV2Repository函数;

    func (p *v2Pusher) pushV2Repository(tag string) error {

        localName := p.repoInfo.LocalName

        if _, err := p.poolAdd("push", localName); err != nil {

            return err

        }

        defer p.poolRemove("push", localName)

        tags, err := p.getImageTags(tag)

        if err != nil {

            return fmt.Errorf("error getting tags for %s: %s", localName, err)

        }

        if len(tags) == 0 {

            return fmt.Errorf("no tags to push for %s", localName)

        }

        for _, tag := range tags {

            if err := p.pushV2Tag(tag); err != nil {

                return err

            }

        } 

        return nil

    }

    调用TagStore的poolAdd函数,将要下载的的名字加入的TagStore的pushingPool里面,保证同一时刻只有一个同名镜像在下载;通过getImageTags(tag)获取对应tag的要下载的镜像,实际上使用pusher的localRepo中查找对应tag的镜像,localRepo 是Repository类型,更确切说是map[string]string类型,key为tag,value为layerID;如果没有传过来的tag为空的话,则返回所有的镜像;接着分析p.pushV2Tag()函数

    func (p *v2Pusher) pushV2Tag(tag string) error {

        logrus.Debugf("Pushing repository: %s:%s", p.repo.Name(), tag)

        layerID, exists := p.localRepo[tag]

        if !exists {

            return fmt.Errorf("tag does not exist: %s", tag)

        }

        layersSeen := make(map[string]bool)

        layer, err := p.graph.Get(layerID)

        if err != nil {

            return err

        }

        m := &manifest.Manifest{

            Versioned: manifest.Versioned{

                SchemaVersion: 1,

            },

            Name:         p.repo.Name(),

            Tag:          tag,

            Architecture: layer.Architecture,

            FSLayers:     []manifest.FSLayer{},

            History:      []manifest.History{},

        }

        var metadata runconfig.Config

        if layer != nil && layer.Config != nil {

            metadata = *layer.Config

        }

        out := p.config.OutStream

        for ; layer != nil; layer, err = p.graph.GetParent(layer) {

            if err != nil {

                return err

            }

            if layersSeen[layer.ID] {

                break

            }

            logrus.Debugf("Pushing layer: %s", layer.ID)

            if layer.Config != nil && metadata.Image != layer.ID {

                if err := runconfig.Merge(&metadata, layer.Config); err != nil {

                    return err

                }

            }

            jsonData, err := p.graph.RawJSON(layer.ID)

            if err != nil {

                return fmt.Errorf("cannot retrieve the path for %s: %s", layer.ID, err)

            }

            var exists bool

            dgst, err := p.graph.GetDigest(layer.ID)

            switch err {

            case nil:

                _, err := p.repo.Blobs(context.Background()).Stat(context.Background(), dgst)

                switch err {

                case nil:

                    exists = true

                    out.Write(p.sf.FormatProgress(stringid.TruncateID(layer.ID), "Image already exists", nil))

                case distribution.ErrBlobUnknown:

                    // nop

                default:

                    out.Write(p.sf.FormatProgress(stringid.TruncateID(layer.ID), "Image push failed", nil))

                    return err

                }

            case ErrDigestNotSet:

                // nop

            case digest.ErrDigestInvalidFormat, digest.ErrDigestUnsupported:

                return fmt.Errorf("error getting image checksum: %v", err)

            }

            // if digest was empty or not saved, or if blob does not exist on the remote repository,

            // then fetch it.

            if !exists {

                if pushDigest, err := p.pushV2Image(p.repo.Blobs(context.Background()), layer); err != nil {

                    return err

                } else if pushDigest != dgst {

                    // Cache new checksum

                    if err := p.graph.SetDigest(layer.ID, pushDigest); err != nil {

                        return err

                    }

                    dgst = pushDigest

                }

            }

            m.FSLayers = append(m.FSLayers, manifest.FSLayer{BlobSum: dgst})

            m.History = append(m.History, manifest.History{V1Compatibility: string(jsonData)})

            layersSeen[layer.ID] = true

        }

        logrus.Infof("Signed manifest for %s:%s using daemon's key: %s", p.repo.Name(), tag, p.trustKey.KeyID())

        signed, err := manifest.Sign(m, p.trustKey)

        if err != nil {

            return err

        }

        manifestDigest, manifestSize, err := digestFromManifest(signed, p.repo.Name())

        if err != nil {

            return err

        }

        if manifestDigest != "" {

            out.Write(p.sf.FormatStatus("", "%s: digest: %s size: %d", tag, manifestDigest, manifestSize))

        }

        manSvc, err := p.repo.Manifests(context.Background())

        if err != nil {

            return err

        }

        return manSvc.Put(signed)

    }

    pushV2Tag()函数比较长,从上到下一点点分析下:首先根据tag从localRepo中找到LayerID,然后通过graph.Get()通过layerID返回layer,实际是一个image结构;layerSeen是一个map,表示的哪些已经被push过了,以免重复;接下来实例化一个manifest结构,manifest主要记录镜像的所有layer信息(包括镜像的校验和以及镜像jsondata中的信息);

    接着就是一个for 循环,从一个layer中不断的取得这个layer的父镜像;然后上传;上传镜像之前,先通过graph获取这个镜像的jsondata和digest校验和,通过 p.repo.Blobs(context.Background()).Stat(context.Background(), dgst) 判断校验和在将要上传的地点是否已经存在,存在的话则打印出相关提示信息,不存在的情况则会调用  p.pushV2Image() 去上传镜像;上传完镜像后会生成新的digest,接着调用

     m.FSLayers = append(m.FSLayers, manifest.FSLayer{BlobSum: dgst})

     m.History = append(m.History, manifest.History{V1Compatibility: string(jsonData)})

     layersSeen[layer.ID] = true

    这三行代码,将layer信息放到manifest文件中,并且设置layerSeen标明这个layer已经上传过了;

    当所有的镜像上传完毕后,接着对manifest文件进行签名、生成manifest文件的digest信息、然后通过repo将manifest信息上传;

    以上就是docker push的大致步骤

  • 相关阅读:
    有一种努力叫“凌晨四点”
    编程思想
    小记
    团队精神与集体主义
    变量起名
    软件项目估量方法
    戏说QQ
    压力说
    AngularJS指令基础(一)
    Leetcode 1021. Best Sightseeing Pair
  • 原文地址:https://www.cnblogs.com/yuhan-TB/p/5276023.html
Copyright © 2020-2023  润新知