以docker pull ubuntu:14.04为例
首先需要创建Docker Client,Docker Client的创建比较简单,这里暂时不说明。当用户输入docker pull Ubuntu:14.04后,进入解析工作,相关代码如下:
if err := cli.Cmd(flag.Args()...); err != nil {
if sterr, ok := err.(*utils.StatusError); ok {
if sterr.Status != "" {
log.Println(sterr.Status)
}
os.Exit(sterr.StatusCode)
}
log.Fatal(err)
}
if err := cli.Cmd(flag.Args()...); err != nil {
if sterr, ok := err.(*utils.StatusError); ok {
if sterr.Status != "" {
log.Println(sterr.Status)
}
os.Exit(sterr.StatusCode)
}
log.Fatal(err)
}
其中,cli.Cmd()函数的具体代码如下:
// Cmd executes the specified command
func (cli *DockerCli) Cmd(args ...string) error {
if len(args) > 0 {
method, exists := cli.getMethod(args[0])
if !exists {
fmt.Println("Error: Command not found:", args[0])
return cli.CmdHelp(args[1:]...)
}
return method(args[1:]...)
}
return cli.CmdHelp(args...)
}
// Cmd executes the specified command
func (cli *DockerCli) Cmd(args ...string) error {
if len(args) > 0 {
method, exists := cli.getMethod(args[0])
if !exists {
fmt.Println("Error: Command not found:", args[0])
return cli.CmdHelp(args[1:]...)
}
return method(args[1:]...)
}
return cli.CmdHelp(args...)
}
根据docker pull ubuntu:14.04命令,函数Cmd里的形参args ...string所对应的实参为pull ubuntu:14.04。如果实参的长度大于0,则继续往下进行。cli调用getMethod处理args[0]即pull,得到具体的处理方法method,getMethod的具体代码如下:
func (cli *DockerCli) getMethod(name string) (func(...string) error, bool) {
if len(name) == 0 {
return nil, false
}
methodName := "Cmd" + strings.ToUpper(name[:1]) + strings.ToLower(name[1:])
method := reflect.ValueOf(cli).MethodByName(methodName)
if !method.IsValid() {
return nil, false
}
return method.Interface().(func(...string) error), true
}
func (cli *DockerCli) getMethod(name string) (func(...string) error, bool) {
if len(name) == 0 {
return nil, false
}
methodName := "Cmd" + strings.ToUpper(name[:1]) + strings.ToLower(name[1:])
method := reflect.ValueOf(cli).MethodByName(methodName)
if !method.IsValid() {
return nil, false
}
return method.Interface().(func(...string) error), true
}
根据以上流程,getMethod返回的方法为CmdPull,实参为ubuntu:14.04.其中,CmdPull函数的具体代码如下的所示:
func (cli *DockerCli) CmdPull(args ...string) error {
cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry")
func (cli *DockerCli) CmdPull(args ...string) error {
cmd := cli.Subcmd("pull", "NAME[:TAG]", "Pull an image or a repository from the registry")
通过cli的Subcmd方法,返回了一个Flagset类型的对象cmd,Subcmd的方法如下所示:
func (cli *DockerCli) Subcmd(name, signature, description string) *flag.FlagSet {
flags := flag.NewFlagSet(name, flag.ContinueOnError)
flags.Usage = func() {
fmt.Fprintf(cli.err, "\nUsage: docker %s %s\n\n%s\n\n", name, signature, description)
flags.PrintDefaults()
os.Exit(2)
}
}
tag := cmd.String([]string{"#t", "#-tag"}, "", "Download tagged image in a repository")
tag := cmd.String([]string{"#t", "#-tag"}, "", "Download tagged image in a repository")
为cmd对象定义了一个类型为string的flag,初始值为空,目前这个flag参数基本已经弃用。
if err := cmd.Parse(args); err != nil {
return nil
}
if err := cmd.Parse(args); err != nil {
return nil
}
对args参数进行解析,此时args的实参为ubuntu:14.04,解析过程中,首先提取是否有符合tag这个flag参数。若有,则赋值给tag参数,其余的参数存入cmd.NArg();若没有,则将所有的参数存入cmd.NArg()中。
if cmd.NArg() != 1 {
cmd.Usage()
return nil
}
if cmd.NArg() != 1 {
cmd.Usage()
return nil
}
判断经过flag解析后的参数列表,若参数个数不为1,则调用错误处理方法cmd.Usage()。ps,在docker的原先版本中是不支持同时下载多个镜像的,docker1.10版本后支持了该功能。
var (
v = url.Values{}
remote = cmd.Arg(0)
)
v.Set("fromImage", remote)
if *tag == "" {
v.Set("tag", *tag)
}
var (
v = url.Values{}
remote = cmd.Arg(0)
)
v.Set("fromImage", remote)
if *tag == "" {
v.Set("tag", *tag)
}
创建一个map类型的变量v,该变量用来存放下拉镜像时所需的URL参数;通过以上设置后,v的值为{"fromImage":ubuntu, "tag":14.04]
remote, _ = parsers.ParseRepositoryTag(remote)
// Resolve the Repository name from fqn to hostname + name
hostname, _, err := registry.ResolveRepositoryName(remote)
if err != nil {
return err
}
cli.LoadConfigFile()
// Resolve the Auth config relevant for this server
authConfig := cli.configFile.ResolveAuthConfig(hostname)
remote, _ = parsers.ParseRepositoryTag(remote)
// Resolve the Repository name from fqn to hostname + name
hostname, _, err := registry.ResolveRepositoryName(remote)
if err != nil {
return err
}
cli.LoadConfigFile()
// Resolve the Auth config relevant for this server
authConfig := cli.configFile.ResolveAuthConfig(hostname)
通过cli对象获得与Docker Server通信所需要的配置信息。
pull := 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/create?"+v.Encode(), nil, cli.out, map[string][]string{
"X-Registry-Auth": registryAuthHeader,
})
}
pull := 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/create?"+v.Encode(), nil, cli.out, map[string][]string{
"X-Registry-Auth": registryAuthHeader,
})
}
定义名为pull的函数,传入的参数类型为registry.AuthConfig,函数最为重要的部分是
cli.stream("POST", "/images/create?"+v.Encode(), nil, cli.out, map[string][]string{
"X-Registry-Auth": registryAuthHeader,
})。
cli的stream函数代码如下所示:
func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, headers map[string][]string) error {
return cli.streamHelper(method, path, true, in, out, nil, headers)
}
func (cli *DockerCli) stream(method, path string, in io.Reader, out io.Writer, headers map[string][]string) error {
return cli.streamHelper(method, path, true, in, out, nil, headers)
}
cli的streamHelper函数代码如下所示:
func (cli *DockerCli) streamHelper(method, path string, setRawTerminal bool, in io.Reader, stdout, stderr io.Writer, headers map[string][]string) error {
if (method == "POST" || method == "PUT") && in == nil {
in = bytes.NewReader([]byte{})
}
req, err := http.NewRequest(method, fmt.Sprintf("http://v%s%s", api.APIVERSION, path), in)
if err != nil {
return err
}
req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION)
req.URL.Host = cli.addr
req.URL.Scheme = cli.scheme
if method == "POST" {
req.Header.Set("Content-Type", "plain/text")
}
if headers != nil {
for k, v := range headers {
req.Header[k] = v
}
}
resp, err := cli.HTTPClient().Do(req)
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?")
}
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if len(body) == 0 {
return fmt.Errorf("Error :%s", http.StatusText(resp.StatusCode))
}
return fmt.Errorf("Error: %s", bytes.TrimSpace(body))
}
if api.MatchesContentType(resp.Header.Get("Content-Type"), "application/json") {
return utils.DisplayJSONMessagesStream(resp.Body, stdout, cli.terminalFd, cli.isTerminal)
}
...
}
func (cli *DockerCli) streamHelper(method, path string, setRawTerminal bool, in io.Reader, stdout, stderr io.Writer, headers map[string][]string) error {
if (method == "POST" || method == "PUT") && in == nil {
in = bytes.NewReader([]byte{})
}
req, err := http.NewRequest(method, fmt.Sprintf("http://v%s%s", api.APIVERSION, path), in)
if err != nil {
return err
}
req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION)
req.URL.Host = cli.addr
req.URL.Scheme = cli.scheme
if method == "POST" {
req.Header.Set("Content-Type", "plain/text")
}
if headers != nil {
for k, v := range headers {
req.Header[k] = v
}
}
resp, err := cli.HTTPClient().Do(req)
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return fmt.Errorf("Cannot connect to the Docker daemon. Is 'docker -d' running on this host?")
}
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if len(body) == 0 {
return fmt.Errorf("Error :%s", http.StatusText(resp.StatusCode))
}
return fmt.Errorf("Error: %s", bytes.TrimSpace(body))
}
if api.MatchesContentType(resp.Header.Get("Content-Type"), "application/json") {
return utils.DisplayJSONMessagesStream(resp.Body, stdout, cli.terminalFd, cli.isTerminal)
}
...
}
在stream函数里构建的请求会发送到docker server,并路由至相应的处理方法。其中,路由规则如下所示:
"POST": {
"/images/create": postImagesCreate,
}
因此,docker client发送过来的请求会进一步的交给postImagesCreate函数处理,具体代码如下所示:
var (
image = r.Form.Get("fromImage")
repo = r.Form.Get("repo")
tag = r.Form.Get("tag")
job *engine.Job
)
首先是解析请求参数,为后续job的运行提供依据‘另外,Docker Server通过从HTTP Header中解析出authEncoded,还原出类型为registry.AuthConfig的对象authConfig,源码如下:
authEncoded := r.Header.Get("X-Registry-Auth")
authConfig := ®istry.AuthConfig{}
if authEncoded != "" {
authJson := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authEncoded))
if err := json.NewDecoder(authJson).Decode(authConfig); err != nil {
// for a pull it is not an error if no auth was given
// to increase compatibility with the existing api it is defaulting to be empty
authConfig = ®istry.AuthConfig{}
}
}
当解析出的image参数不为空的时候,则执行下述代码:
job = eng.Job("pull", image, tag)
job.SetenvBool("parallel", version.GreaterThan("1.3"))
job.SetenvJson("metaHeaders", metaHeaders)
job.SetenvJson("authConfig", authConfig)
eng是docker中处理任务的基本单元job的载体,其中在docker daemon启动的时候已经配置了“pull”所对应的处理方法,实际为graph包中的CmdPull函数,具体代码如下:
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
}
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
}
CmdPull的函数的执行分为以下几个步骤:
var (
localName = job.Args[0]
sf = utils.NewStreamFormatter(job.GetenvBool("json"))
authConfig = ®istry.AuthConfig{}
hostname, remoteName, err := registry.ResolveRepositoryName(localName)
endpoint, err := registry.ExpandAndVerifyRegistryUrl(hostname)
)
localName代表镜像的repository信息
tag代表镜像的tag信息
authConfig代表用户在指定的的Docker Registry上的认证信息
metaHeaders代表请求中的HTTP Headers信息
hostname代表Docker Registry信息
remoteName代表Docker镜像的repository名称信息
endpoint代表Docker Registry完整的URL
在TagStore类型中设计了pullingPool对象,用于保存正在下载的Docker镜像,下载完毕之前禁止其他docker client发起相同镜像的下载请求,下载完毕之后pullingPool中的记录被清楚。
c, err := s.poolAdd("pull", localName+":"+tag)
if err != nil {
if c != nil {
// Another pull of the same repository is already taking place; just wait for it to finish job.Stdout.Write(sf.FormatStatus("", "Repository %s, localName))
<-c
return engine.StatusOK
}
return job.Error(err)
}
defer s.poolRemove("pull", localName+":"+tag)
为了下载docker镜像,docker daemon采用了session机制从docker registry中下载镜像,
r, err := registry.NewSession(authConfig, registry.HTTPRequestFactory(metaHeaders), endpoint, true)
完成以上所有的配置之后,则进入真正的镜像下载阶段。
if err = s.pullRepository(r, job.Stdout, localName, remoteName, tag, sf, job.GetenvBool("parallel")); err != nil {
函数pullRepository的执行流程如下所示:
repoData, err := r.GetRepositoryData(remoteName)
函数GetRepositoryData的作用是获得镜像名称所在repository中所有image的ID信息。Docker Daemon通过RepositoryData和ImageData类型对象来存储这个repository中的所有的image信息。
tagsList, err := r.GetRemoteTags(repoData.Endpoints, remoteName, repoData.Tokens)
函数GetRemoteTags的作用是获取镜像名称所在repository中所有的tag信息。
if err := s.pullImage(r, out, img.ID, ep, repoData.Tokens, sf); err != nil {
函数pullImage的作用是下载镜像,具体流程如下:
history, err := r.GetRemoteHistory(imgID, endpoint, token)
函数GetRemoteHistory的作用是获取指定image及其所有祖先image的id。
imgJSON, imgSize, err = r.GetRemoteImageJSON(id, endpoint, token)
函数GetRemoteImageJSON的作用是得到代表image的json信息imgJSON。
img, err = image.NewImgJSON(imgJSON)
通过imgJSON对象创建一个image对象。
layer, err := r.GetRemoteImageLayer(img.ID, endpoint, token, int64(imgSize))
函数GetRemoteImageLayer的作用是下载镜像layer的内容:该image在parent image之上做的文件系统内容更新,包括文件的增、删、改。
err = s.graph.Register(imgJSON,
utils.ProgressReader(layer, imgSize, out, sf, false, utils.TruncateID(id), "Downloading"),
img)
函数Register完成镜像的存储。
err := s.Set(localName, tag, id, true)
func (store *TagStore) Set(repoName, tag, imageName string, force bool) error {
img, err := store.LookupImage(imageName)
store.Lock()
defer store.Unlock()
if tag == "" {
tag = DEFAULTTAG
}
if err := validateRepoName(repoName); err != nil {
return err
}
if err := validateTagName(tag); err != nil {
...
if err := store.reload(); err != nil
func (store *TagStore) Set(repoName, tag, imageName string, force bool) error {
img, err := store.LookupImage(imageName)
store.Lock()
defer store.Unlock()
if tag == "" {
tag = DEFAULTTAG
}
if err := validateRepoName(repoName); err != nil {
return err
}
if err := validateTagName(tag); err != nil {
...
if err := store.reload(); err != nil