浏览代码

Make container.Copy support volumes
Fixes #1992

Right now when you `docker cp` a path which is in a volume, the cp
itself works, however you end up getting files that are in the
container's fs rather than the files in the volume (which is not in the
container's fs).
This makes it so when you `docker cp` a path that is in a volume it
follows the volume to the real path on the host.

archive.go has been modified so that when you do `docker cp mydata:/foo
.`, and /foo is the volume, the outputed folder is called "foo" instead
of the volume ID (because we are telling it to tar up
`/var/lib/docker/vfs/dir/<some id>` and not "foo", but the user would be
expecting "foo", not the ID

Signed-off-by: Brian Goff <cpuguy83@gmail.com>

Brian Goff 11 年之前
父节点
当前提交
ef98fe0763
共有 5 个文件被更改,包括 176 次插入2 次删除
  1. 8 2
      daemon/container.go
  2. 13 0
      daemon/volumes.go
  3. 107 0
      integration-cli/docker_cli_cp_test.go
  4. 11 0
      pkg/archive/archive.go
  5. 37 0
      volumes/volume.go

+ 8 - 2
daemon/container.go

@@ -826,19 +826,25 @@ func (container *Container) Copy(resource string) (io.ReadCloser, error) {
 		return nil, err
 	}
 
-	var filter []string
-
 	basePath, err := container.getResourcePath(resource)
 	if err != nil {
 		container.Unmount()
 		return nil, err
 	}
 
+	// Check if this is actually in a volume
+	for _, mnt := range container.VolumeMounts() {
+		if len(mnt.MountToPath) > 0 && strings.HasPrefix(resource, mnt.MountToPath[1:]) {
+			return mnt.Export(resource)
+		}
+	}
+
 	stat, err := os.Stat(basePath)
 	if err != nil {
 		container.Unmount()
 		return nil, err
 	}
+	var filter []string
 	if !stat.IsDir() {
 		d, f := path.Split(basePath)
 		basePath = d

+ 13 - 0
daemon/volumes.go

@@ -2,6 +2,7 @@ package daemon
 
 import (
 	"fmt"
+	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -24,6 +25,18 @@ type Mount struct {
 	copyData    bool
 }
 
+func (mnt *Mount) Export(resource string) (io.ReadCloser, error) {
+	var name string
+	if resource == mnt.MountToPath[1:] {
+		name = filepath.Base(resource)
+	}
+	path, err := filepath.Rel(mnt.MountToPath[1:], resource)
+	if err != nil {
+		return nil, err
+	}
+	return mnt.volume.Export(path, name)
+}
+
 func (container *Container) prepareVolumes() error {
 	if container.Volumes == nil || len(container.Volumes) == 0 {
 		container.Volumes = make(map[string]string)

+ 107 - 0
integration-cli/docker_cli_cp_test.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bytes"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -371,3 +372,109 @@ func TestCpUnprivilegedUser(t *testing.T) {
 
 	logDone("cp - unprivileged user")
 }
+
+func TestCpVolumePath(t *testing.T) {
+	tmpDir, err := ioutil.TempDir("", "cp-test-volumepath")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(tmpDir)
+	outDir, err := ioutil.TempDir("", "cp-test-volumepath-out")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(outDir)
+	_, err = os.Create(tmpDir + "/test")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	out, exitCode, err := cmd(t, "run", "-d", "-v", "/foo", "-v", tmpDir+"/test:/test", "-v", tmpDir+":/baz", "busybox", "/bin/sh", "-c", "touch /foo/bar")
+	if err != nil || exitCode != 0 {
+		t.Fatal("failed to create a container", out, err)
+	}
+
+	cleanedContainerID := stripTrailingCharacters(out)
+	defer deleteContainer(cleanedContainerID)
+
+	out, _, err = cmd(t, "wait", cleanedContainerID)
+	if err != nil || stripTrailingCharacters(out) != "0" {
+		t.Fatal("failed to set up container", out, err)
+	}
+
+	// Copy actual volume path
+	_, _, err = cmd(t, "cp", cleanedContainerID+":/foo", outDir)
+	if err != nil {
+		t.Fatalf("couldn't copy from volume path: %s:%s %v", cleanedContainerID, "/foo", err)
+	}
+	stat, err := os.Stat(outDir + "/foo")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !stat.IsDir() {
+		t.Fatal("expected copied content to be dir")
+	}
+	stat, err = os.Stat(outDir + "/foo/bar")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if stat.IsDir() {
+		t.Fatal("Expected file `bar` to be a file")
+	}
+
+	// Copy file nested in volume
+	_, _, err = cmd(t, "cp", cleanedContainerID+":/foo/bar", outDir)
+	if err != nil {
+		t.Fatalf("couldn't copy from volume path: %s:%s %v", cleanedContainerID, "/foo", err)
+	}
+	stat, err = os.Stat(outDir + "/bar")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if stat.IsDir() {
+		t.Fatal("Expected file `bar` to be a file")
+	}
+
+	// Copy Bind-mounted dir
+	_, _, err = cmd(t, "cp", cleanedContainerID+":/baz", outDir)
+	if err != nil {
+		t.Fatalf("couldn't copy from bind-mounted volume path: %s:%s %v", cleanedContainerID, "/baz", err)
+	}
+	stat, err = os.Stat(outDir + "/baz")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !stat.IsDir() {
+		t.Fatal("Expected `baz` to be a dir")
+	}
+
+	// Copy file nested in bind-mounted dir
+	_, _, err = cmd(t, "cp", cleanedContainerID+":/baz/test", outDir)
+	fb, err := ioutil.ReadFile(outDir + "/baz/test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	fb2, err := ioutil.ReadFile(tmpDir + "/test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(fb, fb2) {
+		t.Fatalf("Expected copied file to be duplicate of bind-mounted file")
+	}
+
+	// Copy bind-mounted file
+	_, _, err = cmd(t, "cp", cleanedContainerID+":/test", outDir)
+	fb, err = ioutil.ReadFile(outDir + "/test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	fb2, err = ioutil.ReadFile(tmpDir + "/test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(fb, fb2) {
+		t.Fatalf("Expected copied file to be duplicate of bind-mounted file")
+	}
+
+	logDone("cp - volume path")
+}

+ 11 - 0
pkg/archive/archive.go

@@ -34,6 +34,7 @@ type (
 		Excludes    []string
 		Compression Compression
 		NoLchown    bool
+		Name        string
 	}
 )
 
@@ -359,6 +360,7 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
 		twBuf := pools.BufioWriter32KPool.Get(nil)
 		defer pools.BufioWriter32KPool.Put(twBuf)
 
+		var renamedRelFilePath string // For when tar.Options.Name is set
 		for _, include := range options.Includes {
 			filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error {
 				if err != nil {
@@ -384,6 +386,15 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
 					return nil
 				}
 
+				// Rename the base resource
+				if options.Name != "" && filePath == srcPath+"/"+filepath.Base(relFilePath) {
+					renamedRelFilePath = relFilePath
+				}
+				// Set this to make sure the items underneath also get renamed
+				if options.Name != "" {
+					relFilePath = strings.Replace(relFilePath, renamedRelFilePath, options.Name, 1)
+				}
+
 				if err := addTarFile(filePath, relFilePath, tw, twBuf); err != nil {
 					log.Debugf("Can't add file %s to tar: %s", srcPath, err)
 				}

+ 37 - 0
volumes/volume.go

@@ -2,11 +2,14 @@ package volumes
 
 import (
 	"encoding/json"
+	"io"
 	"io/ioutil"
 	"os"
+	"path"
 	"path/filepath"
 	"sync"
 
+	"github.com/docker/docker/pkg/archive"
 	"github.com/docker/docker/pkg/symlink"
 )
 
@@ -21,6 +24,35 @@ type Volume struct {
 	lock        sync.Mutex
 }
 
+func (v *Volume) Export(resource, name string) (io.ReadCloser, error) {
+	if v.IsBindMount && filepath.Base(resource) == name {
+		name = ""
+	}
+
+	basePath, err := v.getResourcePath(resource)
+	if err != nil {
+		return nil, err
+	}
+	stat, err := os.Stat(basePath)
+	if err != nil {
+		return nil, err
+	}
+	var filter []string
+	if !stat.IsDir() {
+		d, f := path.Split(basePath)
+		basePath = d
+		filter = []string{f}
+	} else {
+		filter = []string{path.Base(basePath)}
+		basePath = path.Dir(basePath)
+	}
+	return archive.TarWithOptions(basePath, &archive.TarOptions{
+		Compression: archive.Uncompressed,
+		Name:        name,
+		Includes:    filter,
+	})
+}
+
 func (v *Volume) IsDir() (bool, error) {
 	stat, err := os.Stat(v.Path)
 	if err != nil {
@@ -137,3 +169,8 @@ func (v *Volume) getRootResourcePath(path string) (string, error) {
 	cleanPath := filepath.Join("/", path)
 	return symlink.FollowSymlinkInScope(filepath.Join(v.configPath, cleanPath), v.configPath)
 }
+
+func (v *Volume) getResourcePath(path string) (string, error) {
+	cleanPath := filepath.Join("/", path)
+	return symlink.FollowSymlinkInScope(filepath.Join(v.Path, cleanPath), v.Path)
+}