Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions libcontainer/configs/config.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package configs

import (
"bytes"
"encoding/json"
"os/exec"
)

type Rlimit struct {
Type int `json:"type"`
Hard uint64 `json:"hard"`
Expand Down Expand Up @@ -159,4 +165,75 @@ 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sorta like how it is in comment. PreStart/PostStop looks better for me.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should match what is in the spec where it is lower case https://github.com/opencontainers/specs/blob/master/runtime_config.go#L24

If we change, then should change there as well and keep it consistent :)

Poststop []Hook
}

// HookState is the payload provided to a hook on execution.
type HookState struct {
ID string `json:"id"`
Pid int `json:"pid"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also pass the path to the root here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, didn't we want to pull in the latest spec to use the state from there?
I don't mind going on with this to unblock user ns for now, but I think we should include the fields from the state there.

}

type Hook interface {
// Run executes the hook with the provided state.
Run(*HookState) error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HookState very simple "immutable" struct. Can be passed everywhere by value to avoid confusion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

}

// 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()
}
7 changes: 0 additions & 7 deletions libcontainer/configs/mount.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
11 changes: 11 additions & 0 deletions libcontainer/container_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -247,6 +248,16 @@ func (c *linuxContainer) Destroy() error {
err = rerr
}
c.initProcess = nil
if c.config.Hooks != nil {
s := configs.HookState{
ID: c.id,
}
for _, hook := range c.config.Hooks.Poststop {
if err := hook.Run(&s); err != nil {
return err
}
}
}
return err
}

Expand Down
49 changes: 49 additions & 0 deletions libcontainer/integration/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -932,3 +932,52 @@ func TestOomScoreAdj(t *testing.T) {
t.Fatalf("Expected oom_score_adj %d; got %q", config.OomScoreAdj, outputOomScoreAdj)
}
}

func TestPrestartHook(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(rootfs, "test"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont think we should reach out to rootfs from here. We should pass it as hook state as @mrunalp mentioned before.

if err != nil {
return err
}
return f.Close()
}),
},
}
container, err := factory.Create("test", config)
ok(t, err)
defer container.Destroy()

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") {
t.Fatal("ls output doesn't have the expected file: ", outputLs)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need test for post stop here :p

12 changes: 12 additions & 0 deletions libcontainer/process_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"syscall"

"github.com/opencontainers/runc/libcontainer/cgroups"
"github.com/opencontainers/runc/libcontainer/configs"
"github.com/opencontainers/runc/libcontainer/system"
)

Expand Down Expand Up @@ -200,6 +201,17 @@ 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(),
}
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)
}
Expand Down