diff --git a/container.go b/container.go index d89f99e474..60c9abbecb 100644 --- a/container.go +++ b/container.go @@ -14,14 +14,17 @@ import ( ) type Container struct { - Name string + Id string Root string + + Created time.Time + Path string Args []string - *Config - *Filesystem - *State + Config *Config + Filesystem *Filesystem + State *State lxcConfigPath string cmd *exec.Cmd @@ -34,10 +37,11 @@ type Config struct { Ram int64 } -func createContainer(name string, root string, command string, args []string, layers []string, config *Config) (*Container, error) { +func createContainer(id string, root string, command string, args []string, layers []string, config *Config) (*Container, error) { container := &Container{ - Name: name, + Id: id, Root: root, + Created: time.Now(), Path: command, Args: args, Config: config, @@ -52,7 +56,6 @@ func createContainer(name string, root string, command string, args []string, la if err := os.Mkdir(root, 0700); err != nil { return nil, err } - if err := container.save(); err != nil { return nil, err } @@ -63,32 +66,23 @@ func createContainer(name string, root string, command string, args []string, la } func loadContainer(containerPath string) (*Container, error) { - configPath := path.Join(containerPath, "config.json") - fi, err := os.Open(configPath) + data, err := ioutil.ReadFile(path.Join(containerPath, "config.json")) if err != nil { return nil, err } - defer fi.Close() - enc := json.NewDecoder(fi) - container := &Container{} - if err := enc.Decode(container); err != nil { + var container *Container + if err := json.Unmarshal(data, container); err != nil { return nil, err } return container, nil } -func (container *Container) save() error { - configPath := path.Join(container.Root, "config.json") - fo, err := os.Create(configPath) +func (container *Container) save() (err error) { + data, err := json.Marshal(container) if err != nil { - return err + return } - defer fo.Close() - enc := json.NewEncoder(fo) - if err := enc.Encode(container); err != nil { - return err - } - return nil + return ioutil.WriteFile(path.Join(container.Root, "config.json"), data, 0700) } func (container *Container) generateLXCConfig() error { @@ -110,7 +104,7 @@ func (container *Container) Start() error { } params := []string{ - "-n", container.Name, + "-n", container.Id, "-f", container.lxcConfigPath, "--", container.Path, @@ -125,21 +119,21 @@ func (container *Container) Start() error { return err } container.State.setRunning(container.cmd.Process.Pid) + container.save() go container.monitor() - - // Wait until we are out of the STARTING state before returning - // - // Even though lxc-wait blocks until the container reaches a given state, - // sometimes it returns an error code, which is why we have to retry. - // - // This is a rare race condition that happens for short lived programs - for retries := 0; retries < 3; retries++ { - err := exec.Command("/usr/bin/lxc-wait", "-n", container.Name, "-s", "RUNNING|STOPPED").Run() - if err == nil { + if err := exec.Command("/usr/bin/lxc-wait", "-n", container.Id, "-s", "RUNNING|STOPPED").Run(); err != nil { + // lxc-wait might return an error if by the time we call it, + // the container we just started is already STOPPED. + // This is a rare race condition that happens for short living programs. + // + // A workaround is to discard lxc-wait errors if the container is not + // running anymore. + if !container.State.Running { return nil } + return errors.New("Container failed to start") } - return errors.New("Container failed to start") + return nil } func (container *Container) Run() error { @@ -177,30 +171,37 @@ func (container *Container) StderrPipe() (io.ReadCloser, error) { } func (container *Container) monitor() { + // Wait for the program to exit container.cmd.Wait() exitCode := container.cmd.ProcessState.Sys().(syscall.WaitStatus).ExitStatus() - // Cleanup container + // Cleanup container.stdout.Close() container.stderr.Close() if err := container.Filesystem.Umount(); err != nil { - log.Printf("%v: Failed to umount filesystem: %v", container.Name, err) + log.Printf("%v: Failed to umount filesystem: %v", container.Id, err) } // Report status back container.State.setStopped(exitCode) + container.save() } func (container *Container) kill() error { // This will cause the main container process to receive a SIGKILL - if err := exec.Command("/usr/bin/lxc-stop", "-n", container.Name).Run(); err != nil { + if err := exec.Command("/usr/bin/lxc-stop", "-n", container.Id).Run(); err != nil { + log.Printf("Failed to lxc-stop %v", container.Id) return err } // Wait for the container to be actually stopped - if err := exec.Command("/usr/bin/lxc-wait", "-n", container.Name, "-s", "STOPPED").Run(); err != nil { - return err - } + container.Wait() + + // Make sure the underlying LXC thinks it's stopped too + // LXC Issue: lxc-wait MIGHT say that the container doesn't exist + // That's probably because it was destroyed and it cannot find it anymore + // We are going to ignore lxc-wait's error + exec.Command("/usr/bin/lxc-wait", "-n", container.Id, "-s", "STOPPED").Run() return nil } @@ -217,13 +218,13 @@ func (container *Container) Stop() error { } // 1. Send a SIGTERM - if err := exec.Command("/usr/bin/lxc-kill", "-n", container.Name, "15").Run(); err != nil { + if err := exec.Command("/usr/bin/lxc-kill", "-n", container.Id, "15").Run(); err != nil { return err } // 2. Wait for the process to exit on its own if err := container.WaitTimeout(10 * time.Second); err != nil { - log.Printf("Container %v failed to exit within 10 seconds of SIGTERM", container.Name) + log.Printf("Container %v failed to exit within 10 seconds of SIGTERM", container.Id) } // 3. Force kill @@ -233,6 +234,16 @@ func (container *Container) Stop() error { return nil } +func (container *Container) Restart() error { + if err := container.Stop(); err != nil { + return err + } + if err := container.Start(); err != nil { + return err + } + return nil +} + func (container *Container) Wait() { for container.State.Running { container.State.wait() diff --git a/container_test.go b/container_test.go index e348a2b4b0..6eedc6c658 100644 --- a/container_test.go +++ b/container_test.go @@ -184,3 +184,59 @@ func TestExitCode(t *testing.T) { t.Errorf("Unexpected exit code %v", falseContainer.State.ExitCode) } } + +func TestMultipleContainers(t *testing.T) { + docker, err := newTestDocker() + if err != nil { + t.Fatal(err) + } + + container1, err := docker.Create( + "container1", + "cat", + []string{"/dev/zero"}, + []string{"/var/lib/docker/images/ubuntu"}, + &Config{}, + ) + if err != nil { + t.Fatal(err) + } + defer docker.Destroy(container1) + + container2, err := docker.Create( + "container2", + "cat", + []string{"/dev/zero"}, + []string{"/var/lib/docker/images/ubuntu"}, + &Config{}, + ) + if err != nil { + t.Fatal(err) + } + defer docker.Destroy(container2) + + // Start both containers + if err := container1.Start(); err != nil { + t.Fatal(err) + } + if err := container2.Start(); err != nil { + t.Fatal(err) + } + + // If we are here, both containers should be running + if !container1.State.Running { + t.Fatal("Container not running") + } + if !container2.State.Running { + t.Fatal("Container not running") + } + + // Kill them + if err := container1.Kill(); err != nil { + t.Fatal(err) + } + + if err := container2.Kill(); err != nil { + t.Fatal(err) + } +} diff --git a/docker.go b/docker.go index 3bdf694664..a1f64c6a95 100644 --- a/docker.go +++ b/docker.go @@ -22,34 +22,34 @@ func (docker *Docker) List() []*Container { return containers } -func (docker *Docker) getContainerElement(name string) *list.Element { +func (docker *Docker) getContainerElement(id string) *list.Element { for e := docker.containers.Front(); e != nil; e = e.Next() { container := e.Value.(*Container) - if container.Name == name { + if container.Id == id { return e } } return nil } -func (docker *Docker) Get(name string) *Container { - e := docker.getContainerElement(name) +func (docker *Docker) Get(id string) *Container { + e := docker.getContainerElement(id) if e == nil { return nil } return e.Value.(*Container) } -func (docker *Docker) Exists(name string) bool { - return docker.Get(name) != nil +func (docker *Docker) Exists(id string) bool { + return docker.Get(id) != nil } -func (docker *Docker) Create(name string, command string, args []string, layers []string, config *Config) (*Container, error) { - if docker.Exists(name) { - return nil, fmt.Errorf("Container %v already exists", name) +func (docker *Docker) Create(id string, command string, args []string, layers []string, config *Config) (*Container, error) { + if docker.Exists(id) { + return nil, fmt.Errorf("Container %v already exists", id) } - root := path.Join(docker.repository, name) - container, err := createContainer(name, root, command, args, layers, config) + root := path.Join(docker.repository, id) + container, err := createContainer(id, root, command, args, layers, config) if err != nil { return nil, err } @@ -58,9 +58,9 @@ func (docker *Docker) Create(name string, command string, args []string, layers } func (docker *Docker) Destroy(container *Container) error { - element := docker.getContainerElement(container.Name) + element := docker.getContainerElement(container.Id) if element == nil { - return fmt.Errorf("Container %v not found - maybe it was already destroyed?", container.Name) + return fmt.Errorf("Container %v not found - maybe it was already destroyed?", container.Id) } if err := container.Stop(); err != nil { diff --git a/docker_test.go b/docker_test.go index 3633ea5a3a..9011a16eac 100644 --- a/docker_test.go +++ b/docker_test.go @@ -51,7 +51,7 @@ func TestCreate(t *testing.T) { } // Make sure the container List() returns is the right one - if docker.List()[0].Name != "test_create" { + if docker.List()[0].Id != "test_create" { t.Errorf("Unexpected container %v returned by List", docker.List()[0]) } diff --git a/filesystem.go b/filesystem.go index 5088590c3a..7a37807790 100644 --- a/filesystem.go +++ b/filesystem.go @@ -1,9 +1,10 @@ package docker import ( + "errors" "fmt" "os" - "os/exec" + "syscall" ) type Filesystem struct { @@ -23,6 +24,9 @@ func (fs *Filesystem) createMountPoints() error { } func (fs *Filesystem) Mount() error { + if fs.IsMounted() { + return errors.New("Mount: Filesystem already mounted") + } if err := fs.createMountPoints(); err != nil { return err } @@ -32,15 +36,33 @@ func (fs *Filesystem) Mount() error { roBranches += fmt.Sprintf("%v=ro:", layer) } branches := fmt.Sprintf("br:%v:%v", rwBranch, roBranches) - cmd := exec.Command("mount", "-t", "aufs", "-o", branches, "none", fs.RootFS) - if err := cmd.Run(); err != nil { - return err - } - return nil + return syscall.Mount("none", fs.RootFS, "aufs", 0, branches) } func (fs *Filesystem) Umount() error { - return exec.Command("umount", fs.RootFS).Run() + if !fs.IsMounted() { + return errors.New("Umount: Filesystem not mounted") + } + return syscall.Unmount(fs.RootFS, 0) +} + +func (fs *Filesystem) IsMounted() bool { + f, err := os.Open(fs.RootFS) + if err != nil { + if os.IsNotExist(err) { + return false + } + panic(err) + } + list, err := f.Readdirnames(1) + f.Close() + if err != nil { + return false + } + if len(list) > 0 { + return true + } + return false } func newFilesystem(rootfs string, rwpath string, layers []string) *Filesystem { diff --git a/filesystem_test.go b/filesystem_test.go index 18aa951852..9c3dd5cff0 100644 --- a/filesystem_test.go +++ b/filesystem_test.go @@ -1,7 +1,10 @@ package docker import ( + "bytes" "io/ioutil" + "os" + "path" "testing" ) @@ -15,7 +18,7 @@ func TestFilesystem(t *testing.T) { t.Fatal(err) } - filesystem := newFilesystem(rootfs, rwpath, []string{"/var/lib/docker/images/ubuntu", "/var/lib/docker/images/test"}) + filesystem := newFilesystem(rootfs, rwpath, []string{"/var/lib/docker/images/ubuntu"}) if err := filesystem.Umount(); err == nil { t.Errorf("Umount succeeded even though the filesystem was not mounted") @@ -25,6 +28,10 @@ func TestFilesystem(t *testing.T) { t.Fatal(err) } + if err := filesystem.Mount(); err == nil { + t.Errorf("Double mount succeeded") + } + if err := filesystem.Umount(); err != nil { t.Fatal(err) } @@ -33,3 +40,48 @@ func TestFilesystem(t *testing.T) { t.Errorf("Umount succeeded even though the filesystem was already umounted") } } + +func TestFilesystemMultiLayer(t *testing.T) { + // Create a fake layer + fakeLayer, err := ioutil.TempDir("", "docker-layer") + if err != nil { + t.Fatal(err) + } + data := []byte("hello world") + if err := ioutil.WriteFile(path.Join(fakeLayer, "test_file"), data, 0700); err != nil { + t.Fatal(err) + } + + // Create the layered filesystem and add our fake layer on top + rootfs, err := ioutil.TempDir("", "docker-test-root") + if err != nil { + t.Fatal(err) + } + rwpath, err := ioutil.TempDir("", "docker-test-rw") + if err != nil { + t.Fatal(err) + } + filesystem := newFilesystem(rootfs, rwpath, []string{"/var/lib/docker/images/ubuntu", fakeLayer}) + + // Mount it + if err := filesystem.Mount(); err != nil { + t.Fatal(err) + } + defer func() { + if err := filesystem.Umount(); err != nil { + t.Fatal(err) + } + }() + + // Check to see whether we can access our fake layer + if _, err := os.Stat(path.Join(rootfs, "test_file")); err != nil { + t.Fatal(err) + } + fsdata, err := ioutil.ReadFile(path.Join(rootfs, "test_file")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(data, fsdata) { + t.Error(string(fsdata)) + } +} diff --git a/lxc_template.go b/lxc_template.go index af9855c663..38aba72b31 100644 --- a/lxc_template.go +++ b/lxc_template.go @@ -9,7 +9,7 @@ const LxcTemplate = ` {{if .Config.Hostname}} lxc.utsname = {{.Config.Hostname}} {{else}} -lxc.utsname = {{.Name}} +lxc.utsname = {{.Id}} {{end}} #lxc.aa_profile = unconfined diff --git a/state.go b/state.go index 80da724851..9b15af0758 100644 --- a/state.go +++ b/state.go @@ -2,12 +2,14 @@ package docker import ( "sync" + "time" ) type State struct { - Running bool - Pid int - ExitCode int + Running bool + Pid int + ExitCode int + StartedAt time.Time stateChangeLock *sync.Mutex stateChangeCond *sync.Cond @@ -25,6 +27,7 @@ func (s *State) setRunning(pid int) { s.Running = true s.ExitCode = 0 s.Pid = pid + s.StartedAt = time.Now() s.broadcast() }