From ef98fe0763024abd90bd5a573fec816895ee92e4 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Wed, 24 Sep 2014 09:07:11 -0400 Subject: [PATCH] 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/` and not "foo", but the user would be expecting "foo", not the ID Signed-off-by: Brian Goff --- daemon/container.go | 10 ++- daemon/volumes.go | 13 ++++ integration-cli/docker_cli_cp_test.go | 107 ++++++++++++++++++++++++++ pkg/archive/archive.go | 11 +++ volumes/volume.go | 37 +++++++++ 5 files changed, 176 insertions(+), 2 deletions(-) diff --git a/daemon/container.go b/daemon/container.go index 6fd4507972..e5c9fadace 100644 --- a/daemon/container.go +++ b/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 diff --git a/daemon/volumes.go b/daemon/volumes.go index c7a8d7bfcb..056d32b548 100644 --- a/daemon/volumes.go +++ b/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) diff --git a/integration-cli/docker_cli_cp_test.go b/integration-cli/docker_cli_cp_test.go index aecc68edb4..b89ddde0b4 100644 --- a/integration-cli/docker_cli_cp_test.go +++ b/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") +} diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go index 7d9103e103..98149160b3 100644 --- a/pkg/archive/archive.go +++ b/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) } diff --git a/volumes/volume.go b/volumes/volume.go index e2d7a726db..73cbb3640d 100644 --- a/volumes/volume.go +++ b/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) +}