Docker的copy命令可以在容器与物理机之间拷贝内容。本次分析将介绍copy命令是如何实现的。
client端
在Docker client端,copy命令由runCopy()执行。runCopy()定义在/api/client/container/cp.go中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| func runCopy(dockerCli *client.DockerCli, opts copyOptions) error { srcContainer, srcPath := splitCpArg(opts.source) dstContainer, dstPath := splitCpArg(opts.destination) var direction copyDirection if srcContainer != "" { direction |= fromContainer } if dstContainer != "" { direction |= toContainer } cpParam := &cpConfig{ followLink: opts.followLink, } ctx := context.Background() switch direction { case fromContainer: return copyFromContainer(ctx, dockerCli, srcContainer, srcPath, dstPath, cpParam) case toContainer: return copyToContainer(ctx, dockerCli, srcPath, dstContainer, dstPath, cpParam) case acrossContainers: return fmt.Errorf("copying between containers is not supported") default: return fmt.Errorf("must specify at least one container source") } }
|
runCopy()会根据copy的参数,如container出现在src中,则调用copyFromContainer();如container出现在dest中,则调用copyToContainer()。
copyFromContainer()可以把内容从容器中拷贝到物理机,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| func copyFromContainer(ctx context.Context, dockerCli *client.DockerCli, srcContainer, srcPath, dstPath string, cpParam *cpConfig) (err error) { ...... content, stat, err := dockerCli.Client().CopyFromContainer(ctx, srcContainer, srcPath) if err != nil { return err } defer content.Close() ...... srcInfo := archive.CopyInfo{ Path: srcPath, Exists: true, IsDir: stat.Mode.IsDir(), RebaseName: rebaseName, } preArchive := content if len(srcInfo.RebaseName) != 0 { _, srcBase := archive.SplitPathDirEntry(srcInfo.Path) preArchive = archive.RebaseArchiveEntries(content, srcBase, srcInfo.RebaseName) } return archive.CopyTo(preArchive, srcInfo, dstPath) }
|
copyFromContainer()先调用Client的CopyFromContainer()获取打包好的content;然后调用archive包的CopyTo()把content的内容解包到dstPath。所以这里出现了archive包的CopyTo()函数,关于archive包,将在下一次分析中介绍。
copyToContainer()可以把物理机上的内容拷贝到容器中,定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| func copyToContainer(ctx context.Context, dockerCli *client.DockerCli, srcPath, dstContainer, dstPath string, cpParam *cpConfig) (err error) { ...... if srcPath == "-" { ...... } else { srcInfo, err := archive.CopyInfoSourcePath(srcPath, cpParam.followLink) if err != nil { return err } srcArchive, err := archive.TarResource(srcInfo) if err != nil { return err } defer srcArchive.Close() ...... dstDir, preparedArchive, err := archive.PrepareArchiveCopy(srcArchive, srcInfo, dstInfo) if err != nil { return err } defer preparedArchive.Close() resolvedDstPath = dstDir content = preparedArchive } ...... return dockerCli.Client().CopyToContainer(ctx, dstContainer, resolvedDstPath, content, options) }
|
copyToContainer()会调用aichive包的TarResource()来把文件源进行打包,然后调用client的CopyToContainer()把打包数据流传入拷贝到容器中。这里出现了archive包的TarResource()函数。
engine-api端
engine-api的client中定义有CopyFromContainer()和CopyToContainer(),都定义在/docker/engine-api/client/container_copy.go中:
CopyFromContainer()定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| func (cli *Client) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) { query := make(url.Values, 1) query.Set("path", filepath.ToSlash(srcPath)) apiPath := fmt.Sprintf("/containers/%s/archive", container) response, err := cli.get(ctx, apiPath, query, nil) if err != nil { return nil, types.ContainerPathStat{}, err } if response.statusCode != http.StatusOK { return nil, types.ContainerPathStat{}, fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) } ...... stat, err := getContainerPathStatFromHeader(response.header) if err != nil { return nil, stat, fmt.Errorf("unable to get resource stat from response: %s", err) } return response.body, stat, err }
|
CopyFromContainer()使用”GET”去请求dockerd的”/containers/container-name/archive”路径。
CopyToContainer()定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| func (cli *Client) CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error { query := url.Values{} query.Set("path", filepath.ToSlash(path)) if !options.AllowOverwriteDirWithFile { query.Set("noOverwriteDirNonDir", "true") } apiPath := fmt.Sprintf("/containers/%s/archive", container) response, err := cli.putRaw(ctx, apiPath, query, content, nil) if err != nil { return err } defer ensureReaderClosed(response) if response.statusCode != http.StatusOK { return fmt.Errorf("unexpected status code from daemon: %d", response.statusCode) } return nil }
|
CopyToContainer把打包数据流以”PUT”方法发送到dockerd的”/containers/container-name/archive”路径。
dockerd侧
在dockerd,”/containers//archive”路径的”GET”请求的功能由getContainersArchive()方法实现;”/containers//archive”路径的”PUT”请求的功能由putContainersArchive()方法实现。
getContainersArchive()
getContainersArchive()定义在/api/server/router/container/copy.go中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| func (s *containerRouter) getContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { v, err := httputils.ArchiveFormValues(r, vars) if err != nil { return err } tarArchive, stat, err := s.backend.ContainerArchivePath(v.Name, v.Path) if err != nil { return err } defer tarArchive.Close() if err := setContainerPathStatHeader(stat, w.Header()); err != nil { return err } w.Header().Set("Content-Type", "application/x-tar") _, err = io.Copy(w, tarArchive) return err }
|
getContainersArchive()会从参数中解析出需要拷贝容器的文件(或目录),然后调用dockerd的ContainerArchivePath()方法把文件(或目录)打包成数据流,然后把数据流的数据通过io.Copy()写入到ResponseWriter中,即应答的body中。
所以关键的实现是ContainerArchivePath()是如何打包文件(或目录)的。ContainerArchivePath()定义在/daemon/archive.go中:
1 2 3 4 5 6 7 8
| func (daemon *Daemon) ContainerArchivePath(name string, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) { container, err := daemon.GetContainer(name) if err != nil { return nil, nil, err } return daemon.containerArchivePath(container, path) }
|
ContainerArchivePath()先获取container,然后调用containerArchivePath()来打包容器中的内容。
containerArchivePath()定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| func (daemon *Daemon) containerArchivePath(container *container.Container, path string) (content io.ReadCloser, stat *types.ContainerPathStat, err error) { container.Lock() defer func() { if err != nil { container.Unlock() } }() if err = daemon.Mount(container); err != nil { return nil, nil, err } defer func() { if err != nil { container.UnmountVolumes(true, daemon.LogVolumeEvent) daemon.Unmount(container) } }() if err = daemon.mountVolumes(container); err != nil { return nil, nil, err } resolvedPath, absPath, err := container.ResolvePath(path) if err != nil { return nil, nil, err } ...... data, err := archive.TarResourceRebase(resolvedPath, filepath.Base(absPath)) if err != nil { return nil, nil, err } content = ioutils.NewReadCloserWrapper(data, func() error { err := data.Close() container.UnmountVolumes(true, daemon.LogVolumeEvent) daemon.Unmount(container) container.Unlock() return err }) daemon.LogContainerEvent(container, "archive-path") return content, stat, nil }
|
containerArchivePath()先解析出resolvedPath和absPath。然后调用archive.TarResourceRebase()来对文件(或目录)进行打包。然后把数据流返回。这里使用了archive包中的TarResourceRebase()。
putContainersArchive()
putContainersArchive()定义在/api/server/router/container/copy.go中:
1 2 3 4 5 6 7 8 9 10 11 12
| func (s *containerRouter) putContainersArchive(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { v, err := httputils.ArchiveFormValues(r, vars) if err != nil { return err } noOverwriteDirNonDir := httputils.BoolValue(r, "noOverwriteDirNonDir") return s.backend.ContainerExtractToDir(v.Name, v.Path, noOverwriteDirNonDir, r.Body) }
|
putContainersArchive()主要调用了dockerd的ContainerExtractToDir()方法。
ContainerExtractToDir()定义在/daemon/archive.go中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| func (daemon *Daemon) containerExtractToDir(container *container.Container, path string, noOverwriteDirNonDir bool, content io.Reader) (err error) { ...... absPath := archive.PreserveTrailingDotOrSeparator(filepath.Join(string(filepath.Separator), path), path) resolvedPath, err := container.GetResourcePath(absPath) if err != nil { return err } ...... uid, gid := daemon.GetRemappedUIDGID() options := &archive.TarOptions{ NoOverwriteDirNonDir: noOverwriteDirNonDir, ChownOpts: &archive.TarChownOptions{ UID: uid, GID: gid, }, } if err := chrootarchive.Untar(content, resolvedPath, options); err != nil { return err } daemon.LogContainerEvent(container, "extract-to-dir") return nil }
|
containerExtractToDir()主要调用的是chrootarchive的Untar()对打包数据流进行解包。
chrootarchive
chrootarchive的Untar()定义在/pkg/chrootarchive/archive.go中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| func Untar(tarArchive io.Reader, dest string, options *archive.TarOptions) error { return untarHandler(tarArchive, dest, options, true) } func untarHandler(tarArchive io.Reader, dest string, options *archive.TarOptions, decompress bool) error { if tarArchive == nil { return fmt.Errorf("Empty archive") } if options == nil { options = &archive.TarOptions{} } if options.ExcludePatterns == nil { options.ExcludePatterns = []string{} } rootUID, rootGID, err := idtools.GetRootUIDGID(options.UIDMaps, options.GIDMaps) if err != nil { return err } dest = filepath.Clean(dest) if _, err := os.Stat(dest); os.IsNotExist(err) { if err := idtools.MkdirAllNewAs(dest, 0755, rootUID, rootGID); err != nil { return err } } r := ioutil.NopCloser(tarArchive) if decompress { decompressedArchive, err := archive.DecompressStream(tarArchive) if err != nil { return err } defer decompressedArchive.Close() r = decompressedArchive } return invokeUnpack(r, dest, options) }
|
Untar()调用了untarHandler(),而untarHandler()主要调用了invokeUnpack()。
invokeUnpack()定义在/pkg/chrootarchive/archive_unix.go中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| func invokeUnpack(decompressedArchive io.Reader, dest string, options *archive.TarOptions) error { r, w, err := os.Pipe() if err != nil { return fmt.Errorf("Untar pipe failure: %v", err) } cmd := reexec.Command("docker-untar", dest) cmd.Stdin = decompressedArchive cmd.ExtraFiles = append(cmd.ExtraFiles, r) output := bytes.NewBuffer(nil) cmd.Stdout = output cmd.Stderr = output if err := cmd.Start(); err != nil { return fmt.Errorf("Untar error on re-exec cmd: %v", err) } if err := json.NewEncoder(w).Encode(options); err != nil { return fmt.Errorf("Untar json encode to pipe failed: %v", err) } w.Close() if err := cmd.Wait(); err != nil { io.Copy(ioutil.Discard, decompressedArchive) return fmt.Errorf("Error processing tar file(%v): %s", err, output) } return nil }
|
这里很有意思,invokeUnpack()调用的是docker-untar(todo: 为什么要通过reexec机制来解包,而不是直接解包,还有待研究),打包数据流以stdin的方式传入。该实现使用Docker的reexec机制,所以必有注册的地方,来看/pkg/chrootarchive/init_unix.go:
1 2 3 4
| func init() { reexec.Register("docker-applyLayer", applyLayer) reexec.Register("docker-untar", untar) }
|
所以docker的”docker-untar”由untar()函数完成功能执行,untar定义在/pkg/chrootarchive/archive_unix.go中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| func untar() { runtime.LockOSThread() flag.Parse() var options *archive.TarOptions if err := json.NewDecoder(os.NewFile(3, "options")).Decode(&options); err != nil { fatal(err) } if err := chroot(flag.Arg(0)); err != nil { fatal(err) } if err := archive.Unpack(os.Stdin, "/", options); err != nil { fatal(err) } if _, err := flush(os.Stdin); err != nil { fatal(err) } os.Exit(0) }
|
untar()会调用archive包的Unpack()实现从Stdin中解包数据。所以这里出现了archive包的Unpack()。Unpack()会把数据流解包到容器指定目录中。
总结
从容器向物理机拷贝:使用archive包的TarResourceRebase()进行容器中文件(或目录)的打包操作;使用archive包的CopyTo()函数完成数据流解包操作;
从物理机向容器拷贝:使用archive包的TarResource()对物理机上文件(或目录)进行打包;使用archive包的Unpack()把数据流解包到容器指定目录中。
下一次分析将介绍Docker的archive包。