diff --git a/container.go b/container.go new file mode 100644 index 000000000..4dc26de88 --- /dev/null +++ b/container.go @@ -0,0 +1,147 @@ +package containerkit + +import ( + "encoding/json" + "io" + "os" + "path/filepath" + "sync" + + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +func NewContainer(root, id string, m Mount, s *specs.Spec, driver ExecutionDriver) (*Container, error) { + path := filepath.Join(root, id) + if err := os.MkdirAll(filepath.Join(path, s.Root.Path), 0711); err != nil { + return nil, err + } + // FIXME: find a better UI for this + s.Mounts = append([]specs.Mount{ + { + Type: m.Type, + Source: m.Source, + Destination: "/", + Options: m.Options, + }, + }, s.Mounts...) + f, err := os.Create(filepath.Join(path, "config.json")) + if err != nil { + return nil, err + } + // write the spec file to the container's directory + err = json.NewEncoder(f).Encode(s) + f.Close() + if err != nil { + return nil, err + } + return &Container{ + id: id, + path: path, + s: s, + driver: driver, + }, nil +} + +type Container struct { + mu sync.Mutex + id string + path string + s *specs.Spec + + driver ExecutionDriver + + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + + // init is the container's init processes + init *Process + // processes is a list of additional processes executed inside the container + // via the NewProcess method on the container + processes []*Process +} + +// ID returns the id of the container +func (c *Container) ID() string { + return c.id +} + +// Path returns the fully qualified path to the container on disk +func (c *Container) Path() string { + return c.path +} + +// Create will create the container on the system by running the runtime's +// initial setup and process waiting for the user process to be started +func (c *Container) Create() error { + c.mu.Lock() + defer c.mu.Unlock() + d, err := c.driver.Create(c) + if err != nil { + return err + } + c.init = &Process{ + d: d, + driver: c.driver, + } + return nil +} + +// Start will start the container's user specified process +func (c *Container) Start() error { + c.mu.Lock() + defer c.mu.Unlock() + return c.driver.Start(c) +} + +// NewProcess will create a new process that will be executed inside the +// container and tied to the init processes lifecycle +func (c *Container) NewProcess(spec *specs.Process) (*Process, error) { + c.mu.Lock() + defer c.mu.Unlock() + process := &Process{ + s: spec, + c: c, + exec: true, + } + c.processes = append(c.processes, process) + return process, nil +} + +// Pid returns the pid of the init or main process hosted inside the container +func (c *Container) Pid() int { + c.mu.Lock() + if c.init == nil { + c.mu.Unlock() + return -1 + } + pid := c.init.Pid() + c.mu.Unlock() + return pid +} + +// Wait will perform a blocking wait on the init process of the container +func (c *Container) Wait() (uint32, error) { + c.mu.Lock() + defer c.mu.Unlock() + return c.init.Wait() +} + +// Signal will send the provided signal to the init process of the container +func (c *Container) Signal(s os.Signal) error { + c.mu.Lock() + defer c.mu.Unlock() + return c.init.Signal(s) +} + +// Delete will delete the container if it no long has any processes running +// inside the container and removes all state on disk for the container +func (c *Container) Delete() error { + c.mu.Lock() + defer c.mu.Unlock() + err := c.driver.Delete(c) + if rerr := os.RemoveAll(c.path); err == nil { + err = rerr + } + return err +} diff --git a/example/main.go b/example/main.go new file mode 100644 index 000000000..2ecdf7d39 --- /dev/null +++ b/example/main.go @@ -0,0 +1,207 @@ +package main + +import ( + "os" + "path/filepath" + "runtime" + + "github.com/Sirupsen/logrus" + "github.com/docker/containerkit" + "github.com/docker/containerkit/osutils" + "github.com/docker/containerkit/runc" + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +var RWM = "rwm" + +// "Hooks do optional work. Drivers do mandatory work" +func main() { + if err := osutils.SetSubreaper(1); err != nil { + logrus.Fatal(err) + } + if err := runTest(); err != nil { + logrus.Fatal(err) + } +} + +func runTest() error { + // create a new runc runtime that implements the ExecutionDriver interface + driver, err := runc.New("/run/runc", "/tmp/runc") + if err != nil { + return err + } + // create a new container + container, err := containerkit.NewContainer( + "/var/lib/containerkit", /* container root */ + "test", /* container id */ + containerkit.Mount{ + Type: "bind", + Source: "/containers/redis/rootfs", + Options: []string{ + "rbind", + "rw", + }, + }, /* mount from the graph subsystem for the container */ + spec("test"), /* the spec for the container */ + driver, /* the exec driver to use for the container */ + ) + if err != nil { + return err + } + // setup some stdio for our container + container.Stdin = os.Stdin + container.Stdout = os.Stdout + container.Stderr = os.Stderr + + // go ahead and set the container in the create state and have it ready to start + logrus.Info("create container") + if err := container.Create(); err != nil { + return err + } + + // start the user defined process in the container + logrus.Info("start container") + if err := container.Start(); err != nil { + return err + } + + // wait for it to exit and get the exit status + logrus.Info("wait container") + status, err := container.Wait() + if err != nil { + return err + } + + // delete the container after it is done + logrus.Info("delete container") + if container.Delete(); err != nil { + return err + } + logrus.Infof("exit status %d", status) + return nil +} + +// bla bla bla spec stuff +func spec(id string) *specs.Spec { + cgpath := filepath.Join("/containerkit", id) + return &specs.Spec{ + Version: specs.Version, + Platform: specs.Platform{ + OS: runtime.GOOS, + Arch: runtime.GOARCH, + }, + Root: specs.Root{ + Path: "rootfs", + Readonly: false, + }, + Process: specs.Process{ + Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + Args: []string{"sleep", "10"}, + Terminal: false, + Cwd: "/", + NoNewPrivileges: true, + Capabilities: []string{ + "CAP_AUDIT_WRITE", + "CAP_KILL", + "CAP_FOWNER", + "CAP_CHOWN", + "CAP_MKNOD", + "CAP_FSETID", + "CAP_DAC_OVERRIDE", + "CAP_SETFCAP", + "CAP_SETPCAP", + "CAP_SETGID", + "CAP_SETUID", + "CAP_NET_BIND_SERVICE", + }, + }, + Hostname: "containerkit", + Mounts: []specs.Mount{ + { + Destination: "/proc", + Type: "proc", + Source: "proc", + }, + { + Destination: "/dev", + Type: "tmpfs", + Source: "tmpfs", + Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}, + }, + { + Destination: "/dev/pts", + Type: "devpts", + Source: "devpts", + Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"}, + }, + { + Destination: "/dev/shm", + Type: "tmpfs", + Source: "shm", + Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"}, + }, + { + Destination: "/dev/mqueue", + Type: "mqueue", + Source: "mqueue", + Options: []string{"nosuid", "noexec", "nodev"}, + }, + { + Destination: "/sys", + Type: "sysfs", + Source: "sysfs", + Options: []string{"nosuid", "noexec", "nodev"}, + }, + { + Destination: "/run", + Type: "tmpfs", + Source: "tmpfs", + Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"}, + }, + { + Destination: "/etc/resolv.conf", + Type: "bind", + Source: "/etc/resolv.conf", + Options: []string{"rbind", "ro"}, + }, + { + Destination: "/etc/hosts", + Type: "bind", + Source: "/etc/hosts", + Options: []string{"rbind", "ro"}, + }, + { + Destination: "/etc/localtime", + Type: "bind", + Source: "/etc/localtime", + Options: []string{"rbind", "ro"}, + }, + }, + Linux: &specs.Linux{ + CgroupsPath: &cgpath, + Resources: &specs.Resources{ + Devices: []specs.DeviceCgroup{ + { + Allow: false, + Access: &RWM, + }, + }, + }, + Namespaces: []specs.Namespace{ + { + Type: "pid", + }, + { + Type: "ipc", + }, + { + Type: "uts", + }, + { + Type: "mount", + }, + }, + }, + } + +} diff --git a/execution.go b/execution.go new file mode 100644 index 000000000..62a61e841 --- /dev/null +++ b/execution.go @@ -0,0 +1,14 @@ +package containerkit + +import "errors" + +var ( + ErrProcessSet = errors.New("containerkit: container process is already set") +) + +type ExecutionDriver interface { + Create(*Container) (ProcessDelegate, error) + Start(*Container) error + Delete(*Container) error + Exec(*Container, *Process) (ProcessDelegate, error) +} diff --git a/process.go b/process.go new file mode 100644 index 000000000..37f30958b --- /dev/null +++ b/process.go @@ -0,0 +1,60 @@ +package containerkit + +import ( + "errors" + "io" + "os" + + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +var ( + ErrNotExecProcess = errors.New("containerkit: process not an exec process") +) + +type ProcessDelegate interface { + Pid() int + Wait() (uint32, error) + Signal(os.Signal) error +} + +type Process struct { + Stdin io.Reader + Stdout io.Writer + Stderr io.Writer + + exec bool + s *specs.Process + + driver ExecutionDriver + c *Container + d ProcessDelegate +} + +func (p *Process) Spec() *specs.Process { + return p.s +} + +func (p *Process) Start() error { + if !p.exec { + return ErrNotExecProcess + } + d, err := p.driver.Exec(p.c, p) + if err != nil { + return err + } + p.d = d + return nil +} + +func (p *Process) Pid() int { + return p.d.Pid() +} + +func (p *Process) Wait() (uint32, error) { + return p.d.Wait() +} + +func (p *Process) Signal(s os.Signal) error { + return p.d.Signal(s) +} diff --git a/runc/runc.go b/runc/runc.go new file mode 100644 index 000000000..0ef44241e --- /dev/null +++ b/runc/runc.go @@ -0,0 +1,117 @@ +package runc + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strconv" + "syscall" + + "github.com/docker/containerkit" +) + +func New(root, log string) (*Runc, error) { + if err := os.MkdirAll(root, 0711); err != nil { + return nil, err + } + return &Runc{ + root: root, + log: log, + }, nil +} + +type Runc struct { + root string + log string +} + +func (r *Runc) Create(c *containerkit.Container) (containerkit.ProcessDelegate, error) { + pidFile := fmt.Sprintf("%s/%s.pid", filepath.Join(r.root, c.ID()), "init") + cmd := r.command("create", "--pid-file", pidFile, "--bundle", c.Path(), c.ID()) + cmd.Stdin, cmd.Stdout, cmd.Stderr = c.Stdin, c.Stdout, c.Stderr + if err := cmd.Run(); err != nil { + return nil, err + } + data, err := ioutil.ReadFile(pidFile) + if err != nil { + return nil, err + } + i, err := strconv.Atoi(string(data)) + if err != nil { + return nil, err + } + return newProcess(i) +} + +func (r *Runc) Start(c *containerkit.Container) error { + return r.command("start", c.ID()).Run() +} + +func (r *Runc) Delete(c *containerkit.Container) error { + return r.command("delete", c.ID()).Run() +} + +func (r *Runc) Exec(c *containerkit.Container, p *containerkit.Process) (containerkit.ProcessDelegate, error) { + f, err := ioutil.TempFile(filepath.Join(r.root, c.ID()), "process") + if err != nil { + return nil, err + } + path := f.Name() + pidFile := fmt.Sprintf("%s/%s.pid", filepath.Join(r.root, c.ID()), filepath.Base(path)) + err = json.NewEncoder(f).Encode(p.Spec()) + f.Close() + if err != nil { + return nil, err + } + cmd := r.command("exec", "--process", path, "--pid-file", pidFile, c.ID()) + cmd.Stdin, cmd.Stdout, cmd.Stderr = p.Stdin, p.Stdout, p.Stderr + data, err := ioutil.ReadFile(pidFile) + if err != nil { + return nil, err + } + i, err := strconv.Atoi(string(data)) + if err != nil { + return nil, err + } + return newProcess(i) +} + +func (r *Runc) command(args ...string) *exec.Cmd { + return exec.Command("runc", append([]string{ + "--root", r.root, + "--log", r.log, + }, args...)...) +} + +func newProcess(pid int) (*process, error) { + proc, err := os.FindProcess(pid) + if err != nil { + return nil, err + } + return &process{ + proc: proc, + }, nil +} + +type process struct { + proc *os.Process +} + +func (p *process) Pid() int { + return p.proc.Pid +} + +func (p *process) Wait() (uint32, error) { + state, err := p.proc.Wait() + if err != nil { + return 0, nil + } + return uint32(state.Sys().(syscall.WaitStatus).ExitStatus()), nil +} + +func (p *process) Signal(s os.Signal) error { + return p.proc.Signal(s) +}