1 背景
NAME
docker-push - Push an image or a repository to the registry
SYNOPSIS
docker push NAME[:TAG]
DESCRIPTION
Push an image or a repository to a registry. The default registry is the Docker Index located at index.docker.io
(https://index.docker.io/v1/). However the image can be pushed to another, perhaps private, registry as demonstrated
in the example below.
本文以私有registry爲例
registry地址:127.0.0.1
port:5000
namespace:name
repository:repo
tag:空
docker version:1.0
docker registry:0.9
2 代碼分析
docker push 127.0.0.1:5000/namespace/repo
2.1 docker client發起push請求
代碼位置:api/client/command.go:CmdPush
此方法是從docker命令行接收參數,並分析命令行所給的參數,給docker http server發送請求。主要解析的是127.0.0.1:5000/namespace/repo, 從這個參數中解析出registry的地址 127.0.0.1:5000,namespace:namespace,repository:repo,tag:空,另外從HOME命令下的.dockercfg中得到auth和email,其中auth是username和password的加密。兩者用於在registry端進行認證,如果找不到.dockercfg文件,docker客戶端會使用空間的user和password發送給registry端。
獲取命令行參數,其中name=127.0.0.1:5000/namespace/repo
name := cmd.Arg(0)
if name == "" {
cmd.Usage()
return nil
}
解析HOME目錄下的.dockercfg文件。首先根據命令行參數的name,解析出remote和tag,此例中,remote=127.0.0.1:5000/namespce/repo,tag=空
再從remote中解析出hostname=127.0.0.1:5000,然後從.dockercfg中找到hostname對應的項,.dockercfg這是以key:value方式存儲的,key就是registry的hostname,value就是auth和email。根據“/”解析namespace和repository是否合法。
<span style="white-space:pre"> </span>cli.LoadConfigFile()
remote, tag := parsers.ParseRepositoryTag(name)
// Resolve the Repository name from fqn to hostname + name
hostname, _, err := registry.ResolveRepositoryName(remote)
if err != nil {
return err
}
// Resolve the Auth config relevant for this server
authConfig := cli.configFile.ResolveAuthConfig(hostname)
// If we're not using a custom registry, we know the restrictions
// applied to repository names and can warn the user in advance.
// Custom repositories can have different rules, and we must also
// allow pushing by image ID.
if len(strings.SplitN(name, "/", 2)) == 1 {
username := cli.configFile.Configs[registry.IndexServerAddress()].Username
if username == "" {
username = "<user>"
}
return fmt.Errorf("You cannot push a \"root\" repository. Please rename your repository in <user>/<repo> (ex: %s/%s)", username, name)
}
在獲取的PUSH http請求的要素後,構建push動作的執行體。將認證信息放在“X-Registry-Auth”頭中。
push := func(authConfig registry.AuthConfig) error {
buf, err := json.Marshal(authConfig)
if err != nil {
return err
}
registryAuthHeader := []string{
base64.URLEncoding.EncodeToString(buf),
}
return cli.stream("POST", "/images/"+remote+"/push?"+v.Encode(), nil, cli.out, map[string][]string{
"X-Registry-Auth": ,
})
}
如果push請求失敗,根據狀態碼是否是401來決定是否發起login請求。在login請求中同樣攜帶auth認證信息。
if err := push(authConfig); err != nil {
if strings.Contains(err.Error(), "Status 401") {
fmt.Fprintln(cli.out, "\nPlease login prior to push:")
if err := cli.CmdLogin(hostname); err != nil {
return err
}
authConfig := cli.configFile.ResolveAuthConfig(hostname)
return push(authConfig)
}
return err
}
2.2 docker httpserver處理push請求
push命令在httpserver的路由項在api/server/server.go中有createRouter方法,它定義了各種請求對應的handler。其中"/images/{name:.*}/push":postImagesPush,在這個方法中,主要解析docker client過來的請求,並設置job環境變量,爲job啓動做準備。postImagesPush的主要任務是解析從客戶端過來的請求,準備job的環境變量,啓動一個job。解析X-Rigestry-Auth頭信息,如何沒有此頭,使用空的auth信息。
authConfig := ®istry.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 = ®istry.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 err
}
}
構建job運行環境,並啓動,這個job也會有engine router找到對應的handler,有handler做實際的執行動作。
job := eng.Job("push", vars["name"])
job.SetenvJson("metaHeaders", metaHeaders)
job.SetenvJson("authConfig", authConfig)
job.Setenv("tag", r.Form.Get("tag"))
if version.GreaterThan("1.0") {
job.SetenvBool("json", true)
streamJSON(job, w, true)
} else {
job.Stdout.Add(utils.NewWriteFlusher(w))
}
if err := job.Run(); err != nil {
if !job.Stdout.Used() {
return err
}
sf := utils.NewStreamFormatter(version.GreaterThan("1.0"))
w.Write(sf.FormatError(err))
}
</pre><h2><span style="font-family:Courier New">2.3 push job</span></h2>
push job的執行體放在graph/push.go文件中。在docker啓動階段,會註冊push job的handler到engine中,關於graph的job處理handler都放在graph中的service.go的install方法中。
job handler的註冊,push使用CmdPush handler。
func (s *TagStore) Install(eng *engine.Engine) error {
for name, handler := range map[string]engine.Handler{
"image_set": s.CmdSet,
"image_tag": s.CmdTag,
"tag": s.CmdTagLegacy, // FIXME merge with "image_tag"
"image_get": s.CmdGet,
"image_inspect": s.CmdLookup,
"image_tarlayer": s.CmdTarLayer,
"image_export": s.CmdImageExport,
"history": s.CmdHistory,
"images": s.CmdImages,
"viz": s.CmdViz,
"load": s.CmdLoad,
"import": s.CmdImport,
"pull": s.CmdPull,
"push": s.CmdPush,
} {
if err := eng.Register(name, handler); err != nil {
return fmt.Errorf("Could not register %q: %v", name, err)
}
}
return nil
}
CmdPush handler放在了graph/push.go文件中。這個handler主要完成想registry發起請求的任務。爲構建項registry請求解析job環境變量中的參數。
var (
localName = job.Args[0]
sf = utils.NewStreamFormatter(job.GetenvBool("json"))
authConfig = ®istry.AuthConfig{}
metaHeaders map[string][]string
)
tag := job.Getenv("tag")
job.GetenvJson("authConfig", authConfig)
job.GetenvJson("metaHeaders", &metaHeaders)
解析registry的hostname和namespace、repository,hostname爲registry的地址"127.0.0.1:5000", remoteName爲"/namespace/repo"
hostname, remoteName, err := registry.ResolveRepositoryName(localName)
檢查registry是否可用,將hostname組裝成http或https的url,做ping操作,檢查是否可用
endpoint, err := registry.ExpandAndVerifyRegistryUrl(hostname)
獲取需要push的鏡像的graph,graph會單獨用一篇文章介紹。
img, err := s.graph.Get(localName)
構建發向registry的請求
r, err2 := registry.NewSession(authConfig, registry.HTTPRequestFactory(metaHeaders), endpoint, false)
if err2 != nil {
return job.Error(err2)
}
如果沒有命令行中沒有tag,會push所有repository下的鏡像,如果有tag,只push tag對應的repository的鏡像
if err != nil {
reposLen := 1
if tag == "" {
reposLen = len(s.Repositories[localName])
}
job.Stdout.Write(sf.FormatStatus("", "The push refers to a repository [%s] (len: %d)", localName, reposLen))
// If it fails, try to get the repository
if localRepo, exists := s.Repositories[localName]; exists {
if err := s.pushRepository(r, job.Stdout, localName, remoteName, localRepo, tag, sf); err != nil {
return job.Error(err)
}
return engine.StatusOK
}
return job.Error(err)
}
var token []string
job.Stdout.Write(sf.FormatStatus("", "The push refers to an image: [%s]", localName))
if _, err := s.pushImage(r, job.Stdout, remoteName, img.ID, endpoint, token, sf); err != nil {
return job.Error(err)
}
3 協議流程
3.1偷偷的ping
3.2 client上傳鏡像的lay id和tag信息
3.3 client端registry請求lay id爲5111的lay json數據,如果registry有此id的json數據,說明registry有此lay,registry給client返回200 OK
如果沒有,registry返回給client 404,client會將此lay的json,lay和checksum上傳給registry
對image的所有lay做上述操作。
3.4上傳image的tag信息,將tag所在的lay id給registry
3.5client最後做put請求images,但裏面什麼也沒有帶,registry發現是空的,就返回給client 204 NO CONTENT,就此push結束(這個地方有點LOW)