* Runtime: better integration of external bind-mounts (run -b) into the volume subsystem (run -v)

This commit is contained in:
Solomon Hykes 2013-06-24 23:30:48 -07:00
parent 4fdf11b2e6
commit d4e62101ab
4 changed files with 195 additions and 216 deletions

View file

@ -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
}
container.Binds = binds
// 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.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")
}

View file

@ -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")
}
}
}

View file

@ -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}}

101
utils_test.go Normal file
View file

@ -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
}