diff --git a/libcontainer/configs/config.go b/libcontainer/configs/config.go index 1179f63cff2..f26287608ab 100644 --- a/libcontainer/configs/config.go +++ b/libcontainer/configs/config.go @@ -1,5 +1,11 @@ package configs +import ( + "bytes" + "encoding/json" + "os/exec" +) + type Rlimit struct { Type int `json:"type"` Hard uint64 `json:"hard"` @@ -159,4 +165,76 @@ type Config struct { // A number of rules are given, each having an action to be taken if a syscall matches it. // A default action to be taken if no rules match is also given. Seccomp *Seccomp `json:"seccomp"` + + // Hooks are a collection of actions to perform at various container lifecycle events. + // Hooks are not able to be marshaled to json but they are also not needed to. + Hooks *Hooks `json:"-"` +} + +type Hooks struct { + // Prestart commands are executed after the container namespaces are created, + // but before the user supplied command is executed from init. + Prestart []Hook + + // Poststop commands are executed after the container init process exits. + Poststop []Hook +} + +// HookState is the payload provided to a hook on execution. +type HookState struct { + ID string `json:"id"` + Pid int `json:"pid"` + Root string `json:"root"` +} + +type Hook interface { + // Run executes the hook with the provided state. + Run(HookState) error +} + +// NewFunctionHooks will call the provided function when the hook is run. +func NewFunctionHook(f func(HookState) error) FuncHook { + return FuncHook{ + run: f, + } +} + +type FuncHook struct { + run func(HookState) error +} + +func (f FuncHook) Run(s HookState) error { + return f.run(s) +} + +type Command struct { + Path string `json:"path"` + Args []string `json:"args"` + Env []string `json:"env"` + Dir string `json:"dir"` +} + +// NewCommandHooks will execute the provided command when the hook is run. +func NewCommandHook(cmd Command) CommandHook { + return CommandHook{ + Command: cmd, + } +} + +type CommandHook struct { + Command +} + +func (c Command) Run(s HookState) error { + b, err := json.Marshal(s) + if err != nil { + return err + } + cmd := exec.Cmd{ + Path: c.Path, + Args: c.Args, + Env: c.Env, + Stdin: bytes.NewReader(b), + } + return cmd.Run() } diff --git a/libcontainer/configs/mount.go b/libcontainer/configs/mount.go index 5a69f815e4e..50668f04fc9 100644 --- a/libcontainer/configs/mount.go +++ b/libcontainer/configs/mount.go @@ -25,10 +25,3 @@ type Mount struct { // Optional Command to be run after Source is mounted. PostmountCmds []Command `json:"postmount_cmds"` } - -type Command struct { - Path string `json:"path"` - Args []string `json:"args"` - Env []string `json:"env"` - Dir string `json:"dir"` -} diff --git a/libcontainer/container_linux.go b/libcontainer/container_linux.go index 9210ec6a3b9..574773bf51a 100644 --- a/libcontainer/container_linux.go +++ b/libcontainer/container_linux.go @@ -185,6 +185,7 @@ func (c *linuxContainer) newInitProcess(p *Process, cmd *exec.Cmd, parentPipe, c parentPipe: parentPipe, manager: c.cgroupManager, config: c.newInitConfig(p), + container: c, }, nil } @@ -247,6 +248,17 @@ func (c *linuxContainer) Destroy() error { err = rerr } c.initProcess = nil + if c.config.Hooks != nil { + s := configs.HookState{ + ID: c.id, + Root: c.config.Rootfs, + } + for _, hook := range c.config.Hooks.Poststop { + if err := hook.Run(s); err != nil { + return err + } + } + } return err } diff --git a/libcontainer/integration/exec_test.go b/libcontainer/integration/exec_test.go index 8e47e087479..bbd1037fa61 100644 --- a/libcontainer/integration/exec_test.go +++ b/libcontainer/integration/exec_test.go @@ -932,3 +932,65 @@ func TestOomScoreAdj(t *testing.T) { t.Fatalf("Expected oom_score_adj %d; got %q", config.OomScoreAdj, outputOomScoreAdj) } } + +func TestHook(t *testing.T) { + if testing.Short() { + return + } + root, err := newTestRoot() + ok(t, err) + defer os.RemoveAll(root) + + rootfs, err := newRootfs() + ok(t, err) + defer remove(rootfs) + + config := newTemplateConfig(rootfs) + config.Hooks = &configs.Hooks{ + Prestart: []configs.Hook{ + configs.NewFunctionHook(func(s configs.HookState) error { + f, err := os.Create(filepath.Join(s.Root, "test")) + if err != nil { + return err + } + return f.Close() + }), + }, + Poststop: []configs.Hook{ + configs.NewFunctionHook(func(s configs.HookState) error { + return os.RemoveAll(filepath.Join(s.Root, "test")) + }), + }, + } + container, err := factory.Create("test", config) + ok(t, err) + + var stdout bytes.Buffer + pconfig := libcontainer.Process{ + Args: []string{"sh", "-c", "ls /test"}, + Env: standardEnvironment, + Stdin: nil, + Stdout: &stdout, + } + err = container.Start(&pconfig) + ok(t, err) + + // Wait for process + waitProcess(&pconfig, t) + + outputLs := string(stdout.Bytes()) + + // Check that the ls output has the expected file touched by the prestart hook + if !strings.Contains(outputLs, "/test") { + container.Destroy() + t.Fatalf("ls output doesn't have the expected file: %s", outputLs) + } + + if err := container.Destroy(); err != nil { + t.Fatalf("container destory %s", err) + } + fi, err := os.Stat(filepath.Join(rootfs, "test")) + if err == nil || !os.IsNotExist(err) { + t.Fatalf("expected file to not exist, got %s", fi.Name()) + } +} diff --git a/libcontainer/process_linux.go b/libcontainer/process_linux.go index 0fe06e8a6a5..f191c16ee29 100644 --- a/libcontainer/process_linux.go +++ b/libcontainer/process_linux.go @@ -13,6 +13,7 @@ import ( "syscall" "github.com/opencontainers/runc/libcontainer/cgroups" + "github.com/opencontainers/runc/libcontainer/configs" "github.com/opencontainers/runc/libcontainer/system" ) @@ -200,6 +201,18 @@ func (p *initProcess) start() (err error) { p.manager.Destroy() } }() + if p.config.Config.Hooks != nil { + s := configs.HookState{ + ID: p.container.id, + Pid: p.pid(), + Root: p.config.Config.Rootfs, + } + for _, hook := range p.config.Config.Hooks.Prestart { + if err := hook.Run(s); err != nil { + return newSystemError(err) + } + } + } if err := p.createNetworkInterfaces(); err != nil { return newSystemError(err) }