diff --git a/components/engine/container.go b/components/engine/container.go index 0524080370..e13747df51 100644 --- a/components/engine/container.go +++ b/components/engine/container.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/dotcloud/docker/archive" "github.com/dotcloud/docker/graphdriver" + "github.com/dotcloud/docker/mount" "github.com/dotcloud/docker/term" "github.com/dotcloud/docker/utils" "github.com/kr/pty" @@ -48,7 +49,6 @@ type Container struct { network *NetworkInterface NetworkSettings *NetworkSettings - SysInitPath string ResolvConfPath string HostnamePath string HostsPath string @@ -297,7 +297,11 @@ func (container *Container) generateEnvConfig(env []string) error { if err != nil { return err } - ioutil.WriteFile(container.EnvConfigPath(), data, 0600) + p, err := container.EnvConfigPath() + if err != nil { + return err + } + ioutil.WriteFile(p, data, 0600) return nil } @@ -681,6 +685,45 @@ func (container *Container) Start() (err error) { } } + root := container.RootfsPath() + envPath, err := container.EnvConfigPath() + if err != nil { + return err + } + + // Mount docker specific files into the containers root fs + if err := mount.Mount(runtime.sysInitPath, path.Join(root, "/.dockerinit"), "none", "bind,ro"); err != nil { + return err + } + if err := mount.Mount(envPath, path.Join(root, "/.dockerenv"), "none", "bind,ro"); err != nil { + return err + } + if err := mount.Mount(container.ResolvConfPath, path.Join(root, "/etc/resolv.conf"), "none", "bind,ro"); err != nil { + return err + } + + if container.HostnamePath != "" && container.HostsPath != "" { + if err := mount.Mount(container.HostnamePath, path.Join(root, "/etc/hostname"), "none", "bind,ro"); err != nil { + return err + } + if err := mount.Mount(container.HostsPath, path.Join(root, "/etc/hosts"), "none", "bind,ro"); err != nil { + return err + } + } + + // Mount user specified volumes + + for r, v := range container.Volumes { + mountAs := "ro" + if container.VolumesRW[v] { + mountAs = "rw" + } + + if err := mount.Mount(v, path.Join(root, r), "none", fmt.Sprintf("bind,%s", mountAs)); err != nil { + return err + } + } + container.cmd = exec.Command(params[0], params[1:]...) // Setup logging of stdout and stderr to disk @@ -1358,6 +1401,32 @@ func (container *Container) GetImage() (*Image, error) { } func (container *Container) Unmount() error { + var ( + err error + root = container.RootfsPath() + mounts = []string{ + path.Join(root, "/.dockerinit"), + path.Join(root, "/.dockerenv"), + path.Join(root, "/etc/resolv.conf"), + } + ) + + if container.HostnamePath != "" && container.HostsPath != "" { + mounts = append(mounts, path.Join(root, "/etc/hostname"), path.Join(root, "/etc/hosts")) + } + + for r := range container.Volumes { + mounts = append(mounts, path.Join(root, r)) + } + + for _, m := range mounts { + if lastError := mount.Unmount(m); lastError != nil { + err = lastError + } + } + if err != nil { + return err + } return container.runtime.Unmount(container) } @@ -1377,8 +1446,20 @@ func (container *Container) jsonPath() string { return path.Join(container.root, "config.json") } -func (container *Container) EnvConfigPath() string { - return path.Join(container.root, "config.env") +func (container *Container) EnvConfigPath() (string, error) { + p := path.Join(container.root, "config.env") + if _, err := os.Stat(p); err != nil { + if os.IsNotExist(err) { + f, err := os.Create(p) + if err != nil { + return "", err + } + f.Close() + } else { + return "", err + } + } + return p, nil } func (container *Container) lxcConfigPath() string { diff --git a/components/engine/graphdriver/aufs/aufs.go b/components/engine/graphdriver/aufs/aufs.go index c3caf13c13..8875cac6d9 100644 --- a/components/engine/graphdriver/aufs/aufs.go +++ b/components/engine/graphdriver/aufs/aufs.go @@ -25,6 +25,7 @@ import ( "fmt" "github.com/dotcloud/docker/archive" "github.com/dotcloud/docker/graphdriver" + mountpk "github.com/dotcloud/docker/mount" "github.com/dotcloud/docker/utils" "os" "os/exec" @@ -295,7 +296,7 @@ func (a *Driver) unmount(id string) error { func (a *Driver) mounted(id string) (bool, error) { target := path.Join(a.rootPath(), "mnt", id) - return Mounted(target) + return mountpk.Mounted(target) } // During cleanup aufs needs to unmount all mountpoints diff --git a/components/engine/graphdriver/aufs/mount.go b/components/engine/graphdriver/aufs/mount.go index 6f3476f99c..1f1d98f809 100644 --- a/components/engine/graphdriver/aufs/mount.go +++ b/components/engine/graphdriver/aufs/mount.go @@ -2,9 +2,7 @@ package aufs import ( "github.com/dotcloud/docker/utils" - "os" "os/exec" - "path/filepath" "syscall" ) @@ -17,21 +15,3 @@ func Unmount(target string) error { } return nil } - -func Mounted(mountpoint string) (bool, error) { - mntpoint, err := os.Stat(mountpoint) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - parent, err := os.Stat(filepath.Join(mountpoint, "..")) - if err != nil { - return false, err - } - mntpointSt := mntpoint.Sys().(*syscall.Stat_t) - parentSt := parent.Sys().(*syscall.Stat_t) - - return mntpointSt.Dev != parentSt.Dev, nil -} diff --git a/components/engine/lxc_template.go b/components/engine/lxc_template.go index ba5a8d5b82..c0dc4f39a9 100644 --- a/components/engine/lxc_template.go +++ b/components/engine/lxc_template.go @@ -21,12 +21,6 @@ lxc.network.mtu = 1500 {{$ROOTFS := .RootfsPath}} lxc.rootfs = {{$ROOTFS}} -{{if and .HostnamePath .HostsPath}} -# enable domain name support -lxc.mount.entry = {{escapeFstabSpaces .HostnamePath}} {{escapeFstabSpaces $ROOTFS}}/etc/hostname none bind,ro 0 0 -lxc.mount.entry = {{escapeFstabSpaces .HostsPath}} {{escapeFstabSpaces $ROOTFS}}/etc/hosts none bind,ro 0 0 -{{end}} - # use a dedicated pts for the container (and limit the number of pseudo terminal # available) lxc.pts = 1024 @@ -74,32 +68,20 @@ lxc.cgroup.devices.allow = c 10:200 rwm # standard mount point # Use mnt.putold as per https://bugs.launchpad.net/ubuntu/+source/lxc/+bug/986385 lxc.pivotdir = lxc_putold + +# NOTICE: These mounts must be applied within the namespace + # WARNING: procfs is a known attack vector and should probably be disabled # if your userspace allows it. eg. see http://blog.zx2c4.com/749 lxc.mount.entry = proc {{escapeFstabSpaces $ROOTFS}}/proc proc nosuid,nodev,noexec 0 0 -# WARNING: sysfs is a known attack vector and should probably be disabled -# if your userspace allows it. eg. see http://bit.ly/T9CkqJ + +# WARNING: sysfs is a known attack vector and should probably be disabled +# if your userspace allows it. eg. see http://bit.ly/T9CkqJ lxc.mount.entry = sysfs {{escapeFstabSpaces $ROOTFS}}/sys sysfs nosuid,nodev,noexec 0 0 + lxc.mount.entry = devpts {{escapeFstabSpaces $ROOTFS}}/dev/pts devpts newinstance,ptmxmode=0666,nosuid,noexec 0 0 -#lxc.mount.entry = varrun {{escapeFstabSpaces $ROOTFS}}/var/run tmpfs mode=755,size=4096k,nosuid,nodev,noexec 0 0 -#lxc.mount.entry = varlock {{escapeFstabSpaces $ROOTFS}}/var/lock tmpfs size=1024k,nosuid,nodev,noexec 0 0 lxc.mount.entry = shm {{escapeFstabSpaces $ROOTFS}}/dev/shm tmpfs size=65536k,nosuid,nodev,noexec 0 0 -# Inject dockerinit -lxc.mount.entry = {{escapeFstabSpaces .SysInitPath}} {{escapeFstabSpaces $ROOTFS}}/.dockerinit none bind,ro 0 0 - -# Inject env -lxc.mount.entry = {{escapeFstabSpaces .EnvConfigPath}} {{escapeFstabSpaces $ROOTFS}}/.dockerenv 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 = {{escapeFstabSpaces .ResolvConfPath}} {{escapeFstabSpaces $ROOTFS}}/etc/resolv.conf none bind,ro 0 0 -{{if .Volumes}} -{{ $rw := .VolumesRW }} -{{range $virtualPath, $realPath := .Volumes}} -lxc.mount.entry = {{escapeFstabSpaces $realPath}} {{escapeFstabSpaces $ROOTFS}}/{{escapeFstabSpaces $virtualPath}} none bind,{{ if index $rw $virtualPath }}rw{{else}}ro{{end}} 0 0 -{{end}} -{{end}} - {{if (getHostConfig .).Privileged}} {{if (getCapabilities .).AppArmor}} lxc.aa_profile = unconfined diff --git a/components/engine/mount/MAINTAINERS b/components/engine/mount/MAINTAINERS new file mode 100644 index 0000000000..1e998f8ac1 --- /dev/null +++ b/components/engine/mount/MAINTAINERS @@ -0,0 +1 @@ +Michael Crosby (@crosbymichael) diff --git a/components/engine/mount/flags_darwin.go b/components/engine/mount/flags_darwin.go new file mode 100644 index 0000000000..e89d5e703a --- /dev/null +++ b/components/engine/mount/flags_darwin.go @@ -0,0 +1,5 @@ +package mount + +func parseOptions(options string) (int, string) { + panic("Not implemented") +} diff --git a/components/engine/mount/flags_linux.go b/components/engine/mount/flags_linux.go new file mode 100644 index 0000000000..6f4c7acffa --- /dev/null +++ b/components/engine/mount/flags_linux.go @@ -0,0 +1,61 @@ +package mount + +import ( + "strings" + "syscall" +) + +// Parse fstab type mount options into mount() flags +// and device specific data +func parseOptions(options string) (int, string) { + var ( + flag int + data []string + ) + + flags := map[string]struct { + clear bool + flag int + }{ + "defaults": {false, 0}, + "ro": {false, syscall.MS_RDONLY}, + "rw": {true, syscall.MS_RDONLY}, + "suid": {true, syscall.MS_NOSUID}, + "nosuid": {false, syscall.MS_NOSUID}, + "dev": {true, syscall.MS_NODEV}, + "nodev": {false, syscall.MS_NODEV}, + "exec": {true, syscall.MS_NOEXEC}, + "noexec": {false, syscall.MS_NOEXEC}, + "sync": {false, syscall.MS_SYNCHRONOUS}, + "async": {true, syscall.MS_SYNCHRONOUS}, + "dirsync": {false, syscall.MS_DIRSYNC}, + "remount": {false, syscall.MS_REMOUNT}, + "mand": {false, syscall.MS_MANDLOCK}, + "nomand": {true, syscall.MS_MANDLOCK}, + "atime": {true, syscall.MS_NOATIME}, + "noatime": {false, syscall.MS_NOATIME}, + "diratime": {true, syscall.MS_NODIRATIME}, + "nodiratime": {false, syscall.MS_NODIRATIME}, + "bind": {false, syscall.MS_BIND}, + "rbind": {false, syscall.MS_BIND | syscall.MS_REC}, + "relatime": {false, syscall.MS_RELATIME}, + "norelatime": {true, syscall.MS_RELATIME}, + "strictatime": {false, syscall.MS_STRICTATIME}, + "nostrictatime": {true, syscall.MS_STRICTATIME}, + } + + for _, o := range strings.Split(options, ",") { + // If the option does not exist in the flags table then it is a + // data value for a specific fs type + if f, exists := flags[o]; exists { + if f.clear { + flag &= ^f.flag + } else { + flag |= f.flag + } + } else { + data = append(data, o) + } + } + return flag, strings.Join(data, ",") +} diff --git a/components/engine/mount/mount.go b/components/engine/mount/mount.go new file mode 100644 index 0000000000..253a4681ef --- /dev/null +++ b/components/engine/mount/mount.go @@ -0,0 +1,53 @@ +package mount + +import ( + "time" +) + +// Looks at /proc/self/mountinfo to determine of the specified +// mountpoint has been mounted +func Mounted(mountpoint string) (bool, error) { + entries, err := parseMountTable() + if err != nil { + return false, err + } + + // Search the table for the mountpoint + for _, e := range entries { + if e.mountpoint == mountpoint { + return true, nil + } + } + return false, nil +} + +// Mount the specified options at the target path +// Options must be specified as fstab style +func Mount(device, target, mType, options string) error { + if mounted, err := Mounted(target); err != nil || mounted { + return err + } + + flag, data := parseOptions(options) + if err := mount(device, target, mType, uintptr(flag), data); err != nil { + return err + } + return nil + +} + +// Unmount the target only if it is mounted +func Unmount(target string) (err error) { + if mounted, err := Mounted(target); err != nil || !mounted { + return err + } + + // Simple retry logic for unmount + for i := 0; i < 10; i++ { + if err = unmount(target, 0); err == nil { + return nil + } + time.Sleep(100 * time.Millisecond) + } + return +} diff --git a/components/engine/mount/mount_test.go b/components/engine/mount/mount_test.go new file mode 100644 index 0000000000..5dc9dc256a --- /dev/null +++ b/components/engine/mount/mount_test.go @@ -0,0 +1,67 @@ +package mount + +import ( + "os" + "path" + "syscall" + "testing" +) + +func TestMountOptionsParsing(t *testing.T) { + options := "bind,ro,size=10k" + + flag, data := parseOptions(options) + + if data != "size=10k" { + t.Fatalf("Expected size=10 got %s", data) + } + + expectedFlag := syscall.MS_BIND | syscall.MS_RDONLY + + if flag != expectedFlag { + t.Fatalf("Expected %d got %d", expectedFlag, flag) + } +} + +func TestMounted(t *testing.T) { + tmp := path.Join(os.TempDir(), "mount-tests") + if err := os.MkdirAll(tmp, 0777); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + var ( + sourcePath = path.Join(tmp, "sourcefile.txt") + targetPath = path.Join(tmp, "targetfile.txt") + ) + + f, err := os.Create(sourcePath) + if err != nil { + t.Fatal(err) + } + f.WriteString("hello") + f.Close() + + f, err = os.Create(targetPath) + if err != nil { + t.Fatal(err) + } + f.Close() + + if err := Mount(sourcePath, targetPath, "none", "bind,ro"); err != nil { + t.Fatal(err) + } + defer func() { + if err := Unmount(targetPath); err != nil { + t.Fatal(err) + } + }() + + mounted, err := Mounted(targetPath) + if err != nil { + t.Fatal(err) + } + if !mounted { + t.Fatalf("Expected %s to be mounted", targetPath) + } +} diff --git a/components/engine/mount/mounter_darwin.go b/components/engine/mount/mounter_darwin.go new file mode 100644 index 0000000000..7615f94f9e --- /dev/null +++ b/components/engine/mount/mounter_darwin.go @@ -0,0 +1,9 @@ +package mount + +func mount(device, target, mType string, flag uintptr, data string) error { + panic("Not implemented") +} + +func unmount(target string, flag int) error { + panic("Not implemented") +} diff --git a/components/engine/mount/mounter_linux.go b/components/engine/mount/mounter_linux.go new file mode 100644 index 0000000000..1371f72bd9 --- /dev/null +++ b/components/engine/mount/mounter_linux.go @@ -0,0 +1,13 @@ +package mount + +import ( + "syscall" +) + +func mount(device, target, mType string, flag uintptr, data string) error { + return syscall.Mount(device, target, mType, flag, data) +} + +func unmount(target string, flag int) error { + return syscall.Unmount(target, flag) +} diff --git a/components/engine/mount/mountinfo.go b/components/engine/mount/mountinfo.go new file mode 100644 index 0000000000..397e792259 --- /dev/null +++ b/components/engine/mount/mountinfo.go @@ -0,0 +1,45 @@ +package mount + +import ( + "bufio" + "fmt" + "os" +) + +const ( + mountinfoFormat = "%d %d %d:%d %s %s %s - %s %s %s" +) + +// Represents one line from /proc/self/mountinfo +type procEntry struct { + id, parent, major, minor int + source, mountpoint, fstype, device string + vfsopts, opts string +} + +// Parse /proc/self/mountinfo because comparing Dev and ino does not work from bind mounts +func parseMountTable() ([]*procEntry, error) { + f, err := os.Open("/proc/self/mountinfo") + if err != nil { + return nil, err + } + defer f.Close() + + s := bufio.NewScanner(f) + out := []*procEntry{} + for s.Scan() { + if err := s.Err(); err != nil { + return nil, err + } + + p := &procEntry{} + if _, err := fmt.Sscanf(s.Text(), mountinfoFormat, + &p.id, &p.parent, &p.major, &p.minor, + &p.source, &p.mountpoint, &p.vfsopts, &p.fstype, + &p.device, &p.opts); err != nil { + return nil, err + } + out = append(out, p) + } + return out, nil +} diff --git a/components/engine/runtime.go b/components/engine/runtime.go index a6233339cb..588e713ca5 100644 --- a/components/engine/runtime.go +++ b/components/engine/runtime.go @@ -485,10 +485,8 @@ func (runtime *Runtime) Create(config *Config, name string) (*Container, []strin hostConfig: &HostConfig{}, Image: img.ID, // Always use the resolved image id NetworkSettings: &NetworkSettings{}, - // FIXME: do we need to store this in the container? - SysInitPath: runtime.sysInitPath, - Name: name, - Driver: runtime.driver.String(), + Name: name, + Driver: runtime.driver.String(), } container.root = runtime.containerRoot(container.ID) // Step 1: create the container directory.