diff --git a/container.go b/container.go index 77f71ba684bd2..64c4a80e3fa64 100644 --- a/container.go +++ b/container.go @@ -52,8 +52,9 @@ type Container struct { waitLock chan struct{} Volumes map[string]string - - Binds []BindMap + // Store rw/ro in a separate structure to preserve reserve-compatibility on-disk. + // Easier than migrating older container configs :) + VolumesRW map[string]bool } type Config struct { @@ -481,41 +482,15 @@ func (container *Container) Start(hostConfig *HostConfig) error { container.Config.MemorySwap = -1 } container.Volumes = make(map[string]string) - - // Create the requested volumes volumes - for volPath := range container.Config.Volumes { - c, err := container.runtime.volumes.Create(nil, container, "", "", nil) - if err != nil { - return err - } - if err := os.MkdirAll(path.Join(container.RootfsPath(), volPath), 0755); err != nil { - return nil - } - container.Volumes[volPath] = c.ID - } - - if container.Config.VolumesFrom != "" { - c := container.runtime.Get(container.Config.VolumesFrom) - if c == nil { - return fmt.Errorf("Container %s not found. Impossible to mount its volumes", container.ID) - } - for volPath, id := range c.Volumes { - if _, exists := container.Volumes[volPath]; exists { - return fmt.Errorf("The requested volume %s overlap one of the volume of the container %s", volPath, c.ID) - } - if err := os.MkdirAll(path.Join(container.RootfsPath(), volPath), 0755); err != nil { - return nil - } - container.Volumes[volPath] = id - } - } + container.VolumesRW = make(map[string]bool) // Create the requested bind mounts - binds := []BindMap{} + binds := make(map[string]BindMap) // Define illegal container destinations illegal_dsts := []string{"/", "."} for _, bind := range hostConfig.Binds { + // FIXME: factorize bind parsing in parseBind var src, dst, mode string arr := strings.Split(bind, ":") if len(arr) == 2 { @@ -542,9 +517,54 @@ func (container *Container) Start(hostConfig *HostConfig) error { DstPath: dst, Mode: mode, } - binds = append(binds, bindMap) + binds[path.Clean(dst)] = bindMap + } + + // FIXME: evaluate volumes-from before individual volumes, so that the latter can override the former. + // Create the requested volumes volumes + for volPath := range container.Config.Volumes { + volPath = path.Clean(volPath) + // If an external bind is defined for this volume, use that as a source + if bindMap, exists := binds[volPath]; exists { + container.Volumes[volPath] = bindMap.SrcPath + if strings.ToLower(bindMap.Mode) == "rw" { + container.VolumesRW[volPath] = true + } + // Otherwise create an directory in $ROOT/volumes/ and use that + } else { + c, err := container.runtime.volumes.Create(nil, container, "", "", nil) + if err != nil { + return err + } + srcPath, err := c.layer() + if err != nil { + return err + } + container.Volumes[volPath] = srcPath + container.VolumesRW[volPath] = true // RW by default + } + // Create the mountpoint + if err := os.MkdirAll(path.Join(container.RootfsPath(), volPath), 0755); err != nil { + return nil + } + } + + if container.Config.VolumesFrom != "" { + c := container.runtime.Get(container.Config.VolumesFrom) + if c == nil { + return fmt.Errorf("Container %s not found. Impossible to mount its volumes", container.ID) + } + for volPath, id := range c.Volumes { + if _, exists := container.Volumes[volPath]; exists { + return fmt.Errorf("The requested volume %s overlap one of the volume of the container %s", volPath, c.ID) + } + if err := os.MkdirAll(path.Join(container.RootfsPath(), volPath), 0755); err != nil { + return nil + } + container.Volumes[volPath] = id + } } - container.Binds = binds + container.VolumesRW = make(map[string]bool) if err := container.generateLXCConfig(); err != nil { return err @@ -957,22 +977,6 @@ func (container *Container) RootfsPath() string { return path.Join(container.root, "rootfs") } -func (container *Container) GetVolumes() (map[string]string, error) { - ret := make(map[string]string) - for volPath, id := range container.Volumes { - volume, err := container.runtime.volumes.Get(id) - if err != nil { - return nil, err - } - root, err := volume.root() - if err != nil { - return nil, err - } - ret[volPath] = path.Join(root, "layer") - } - return ret, nil -} - func (container *Container) rwPath() string { return path.Join(container.root, "rw") } diff --git a/container_test.go b/container_test.go index cefbd8cb052a8..cb8c9c860d401 100644 --- a/container_test.go +++ b/container_test.go @@ -16,10 +16,7 @@ import ( ) func TestIDFormat(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container1, err := NewBuilder(runtime).Create( &Config{ @@ -40,10 +37,7 @@ func TestIDFormat(t *testing.T) { } func TestMultipleAttachRestart(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container, err := NewBuilder(runtime).Create( &Config{ @@ -144,10 +138,7 @@ func TestMultipleAttachRestart(t *testing.T) { } func TestDiff(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) builder := NewBuilder(runtime) @@ -253,10 +244,7 @@ func TestDiff(t *testing.T) { } func TestCommitAutoRun(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) builder := NewBuilder(runtime) @@ -333,10 +321,7 @@ func TestCommitAutoRun(t *testing.T) { } func TestCommitRun(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) builder := NewBuilder(runtime) @@ -416,10 +401,7 @@ func TestCommitRun(t *testing.T) { } func TestStart(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container, err := NewBuilder(runtime).Create( &Config{ @@ -461,10 +443,7 @@ func TestStart(t *testing.T) { } func TestRun(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container, err := NewBuilder(runtime).Create( &Config{ @@ -489,10 +468,7 @@ func TestRun(t *testing.T) { } func TestOutput(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container, err := NewBuilder(runtime).Create( &Config{ @@ -514,10 +490,7 @@ func TestOutput(t *testing.T) { } func TestKillDifferentUser(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container, err := NewBuilder(runtime).Create(&Config{ Image: GetTestImage(runtime).ID, @@ -564,10 +537,7 @@ func TestKillDifferentUser(t *testing.T) { // Test that creating a container with a volume doesn't crash. Regression test for #995. func TestCreateVolume(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) config, _, err := ParseRun([]string{"-v", "/var/lib/data", GetTestImage(runtime).ID, "echo", "hello", "world"}, nil) @@ -587,10 +557,7 @@ func TestCreateVolume(t *testing.T) { } func TestKill(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container, err := NewBuilder(runtime).Create(&Config{ Image: GetTestImage(runtime).ID, @@ -633,10 +600,7 @@ func TestKill(t *testing.T) { } func TestExitCode(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) builder := NewBuilder(runtime) @@ -673,10 +637,7 @@ func TestExitCode(t *testing.T) { } func TestRestart(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container, err := NewBuilder(runtime).Create(&Config{ Image: GetTestImage(runtime).ID, @@ -706,10 +667,7 @@ func TestRestart(t *testing.T) { } func TestRestartStdin(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container, err := NewBuilder(runtime).Create(&Config{ Image: GetTestImage(runtime).ID, @@ -785,10 +743,7 @@ func TestRestartStdin(t *testing.T) { } func TestUser(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) builder := NewBuilder(runtime) @@ -895,10 +850,7 @@ func TestUser(t *testing.T) { } func TestMultipleContainers(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) builder := NewBuilder(runtime) @@ -955,10 +907,7 @@ func TestMultipleContainers(t *testing.T) { } func TestStdin(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container, err := NewBuilder(runtime).Create(&Config{ Image: GetTestImage(runtime).ID, @@ -1003,10 +952,7 @@ func TestStdin(t *testing.T) { } func TestTty(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container, err := NewBuilder(runtime).Create(&Config{ Image: GetTestImage(runtime).ID, @@ -1051,10 +997,7 @@ func TestTty(t *testing.T) { } func TestEnv(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) container, err := NewBuilder(runtime).Create(&Config{ Image: GetTestImage(runtime).ID, @@ -1121,10 +1064,7 @@ func grepFile(t *testing.T, path string, pattern string) { } func TestLXCConfig(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } + runtime := mkRuntime(t) defer nuke(runtime) // Memory is allocated randomly for testing rand.Seed(time.Now().UTC().UnixNano()) @@ -1239,97 +1179,34 @@ func BenchmarkRunParallel(b *testing.B) { } } -func TestBindMounts(t *testing.T) { - runtime, err := newTestRuntime() - if err != nil { - t.Fatal(err) - } - defer nuke(runtime) +func tempDir(t *testing.T) string { tmpDir, err := ioutil.TempDir("", "docker-test") if err != nil { t.Fatal(err) } - tmpFile := path.Join(tmpDir, "touch-me") - _, err = os.Create(tmpFile) - if err != nil { - t.Fatal(err) - } - // test reading from bind mount - bind_str := fmt.Sprintf("%s:/tmp:ro", tmpDir) - container, err := NewBuilder(runtime).Create(&Config{ - Image: GetTestImage(runtime).ID, - Cmd: []string{"ls", "/tmp"}, - }, - ) - if err != nil { - t.Fatal(err) - } - defer runtime.Destroy(container) + return tmpDir +} - stdout, err := container.StdoutPipe() - if err != nil { - t.Fatal(err) - } - defer stdout.Close() - hostConfig := &HostConfig{ - Binds: []string{bind_str}, - } - if err := container.Start(hostConfig); err != nil { - t.Fatal(err) - } - container.Wait() - output, err := ioutil.ReadAll(stdout) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(string(output), "touch-me") { +func TestBindMounts(t *testing.T) { + r := mkRuntime(t) + defer nuke(r) + tmpDir := tempDir(t) + defer os.RemoveAll(tmpDir) + writeFile(path.Join(tmpDir, "touch-me"), "", t) + + // Test reading from a read-only bind mount + stdout, _ := runContainer(r, []string{"-b", fmt.Sprintf("%s:/tmp:ro", tmpDir), "_", "ls", "/tmp"}, t) + if !strings.Contains(stdout, "touch-me") { t.Fatal("Container failed to read from bind mount") } + // test writing to bind mount - bind_str2 := fmt.Sprintf("%s:/tmp:rw", tmpDir) - container2, err := NewBuilder(runtime).Create(&Config{ - Image: GetTestImage(runtime).ID, - Cmd: []string{"touch", "/tmp/holla"}, - }, - ) - if err != nil { - t.Fatal(err) - } - defer runtime.Destroy(container2) + runContainer(r, []string{"-b", fmt.Sprintf("%s:/tmp:rw", tmpDir), "_", "touch", "/tmp/holla"}, t) + readFile("/tmp/holla", t) // Will fail if the file doesn't exist - hostConfig2 := &HostConfig{ - Binds: []string{bind_str2}, - } - if err := container2.Start(hostConfig2); err != nil { - t.Fatal(err) - } - container2.Wait() - _, err = ioutil.ReadFile(tmpDir + "/holla") - if err != nil { - t.Fatal("Container failed to write to bind mount") - } // test mounting to an illegal destination directory - bind_str3 := fmt.Sprintf("%s:.", tmpDir) - container3, err := NewBuilder(runtime).Create(&Config{ - Image: GetTestImage(runtime).ID, - Cmd: []string{"ls", "."}, - }, - ) - if err != nil { - t.Fatal(err) - } - defer runtime.Destroy(container3) - - stdout3, err := container3.StdoutPipe() - if err != nil { - t.Fatal(err) - } - defer stdout3.Close() - hostConfig3 := &HostConfig{ - Binds: []string{bind_str3}, - } - if err := container3.Start(hostConfig3); err == nil { + if _, err := runContainer(r, []string{"-b", fmt.Sprintf("%s:.", tmpDir), "ls", "."}, nil); err == nil { t.Fatal("Container bind mounted illegal directory") - } + } } diff --git a/lxc_template.go b/lxc_template.go index 3f15b9abbadf1..b3fed74d17bf3 100644 --- a/lxc_template.go +++ b/lxc_template.go @@ -84,12 +84,9 @@ lxc.mount.entry = {{.SysInitPath}} {{$ROOTFS}}/sbin/init none bind,ro 0 0 # In order to get a working DNS environment, mount bind (ro) the host's /etc/resolv.conf into the container lxc.mount.entry = {{.ResolvConfPath}} {{$ROOTFS}}/etc/resolv.conf none bind,ro 0 0 {{if .Volumes}} -{{range $virtualPath, $realPath := .GetVolumes}} -lxc.mount.entry = {{$realPath}} {{$ROOTFS}}/{{$virtualPath}} none bind,rw 0 0 -{{end}} -{{end}} -{{if .Binds}}# User-defined bind mounts -{{range $bindMap := .Binds}}lxc.mount.entry = {{$bindMap.SrcPath}} {{$ROOTFS}}{{$bindMap.DstPath}} none bind,{{$bindMap.Mode}} 0 0 ++{{ $rw := .VolumesRW }} ++{{range $virtualPath, $realPath := .Volumes}} ++lxc.mount.entry = {{$realPath}} {{$ROOTFS}}/{{$virtualPath}} none bind,{{ if index $rw $virtualPath }}rw{{else}}ro{{end}} 0 0 {{end}} {{end}} diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 0000000000000..20fa2ee2dbd7c --- /dev/null +++ b/utils_test.go @@ -0,0 +1,101 @@ +package docker + +import ( + "io" + "io/ioutil" + "os" + "path" + "strings" + "testing" +) + +// This file contains utility functions for docker's unit test suite. +// It has to be named XXX_test.go, apparently, in other to access private functions +// from other XXX_test.go functions. + +// Create a temporary runtime suitable for unit testing. +// Call t.Fatal() at the first error. +func mkRuntime(t *testing.T) *Runtime { + runtime, err := newTestRuntime() + if err != nil { + t.Fatal(err) + } + return runtime +} + +// Write `content` to the file at path `dst`, creating it if necessary, +// as well as any missing directories. +// The file is truncated if it already exists. +// Call t.Fatal() at the first error. +func writeFile(dst, content string, t *testing.T) { + // Create subdirectories if necessary + if err := os.MkdirAll(path.Dir(dst), 0700); err != nil && !os.IsExist(err) { + t.Fatal(err) + } + f, err := os.OpenFile(dst, os.O_RDWR|os.O_TRUNC, 0700) + if err != nil { + t.Fatal(err) + } + // Write content (truncate if it exists) + if _, err := io.Copy(f, strings.NewReader(content)); err != nil { + t.Fatal(err) + } +} + +// Return the contents of file at path `src`. +// Call t.Fatal() at the first error (including if the file doesn't exist) +func readFile(src string, t *testing.T) (content string) { + f, err := os.Open(src) + if err != nil { + t.Fatal(err) + } + data, err := ioutil.ReadAll(f) + if err != nil { + t.Fatal(err) + } + return string(data) +} + +// Create a test container from the given runtime `r` and run arguments `args`. +// The caller is responsible for destroying the container. +// Call t.Fatal() at the first error. +func mkContainer(r *Runtime, args []string, t *testing.T) (*Container, *HostConfig) { + config, hostConfig, _, err := ParseRun(args, nil) + if err != nil { + t.Fatal(err) + } + c, err := NewBuilder(r).Create(config) + if err != nil { + t.Fatal(err) + } + c.Image = GetTestImage(r).ID + return c, hostConfig +} + +// Create a test container, start it, wait for it to complete, destroy it, +// and return its standard output as a string. +// If t is not nil, call t.Fatal() at the first error. Otherwise return errors normally. +func runContainer(r *Runtime, args []string, t *testing.T) (output string, err error) { + defer func() { + if err != nil && t != nil { + t.Fatal(err) + } + }() + container, hostConfig := mkContainer(r, args, t) + defer r.Destroy(container) + stdout, err := container.StdoutPipe() + if err != nil { + return "", err + } + defer stdout.Close() + if err := container.Start(hostConfig); err != nil { + return "", err + } + container.Wait() + data, err := ioutil.ReadAll(stdout) + if err != nil { + return "", err + } + output = string(data) + return +}