Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement CreateRuntime, Poststop hooks #46

Merged
merged 2 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions cmd/runj/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"path/filepath"
"strconv"

"go.sbk.wtf/runj/hook"
"go.sbk.wtf/runj/jail"
"go.sbk.wtf/runj/oci"
"go.sbk.wtf/runj/runtimespec"
Expand Down Expand Up @@ -214,6 +215,17 @@ written`)
}
}

if ociConfig.Hooks != nil {
for _, h := range ociConfig.Hooks.CreateRuntime {
output := s.Output()
output.Annotations = ociConfig.Annotations
err = hook.Run(&output, &h)
if err != nil {
return err
}
}
}

return nil
}
return create
Expand Down
24 changes: 23 additions & 1 deletion cmd/runj/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"

"go.sbk.wtf/runj/hook"
"go.sbk.wtf/runj/jail"
"go.sbk.wtf/runj/oci"
"go.sbk.wtf/runj/runtimespec"
Expand Down Expand Up @@ -32,6 +33,10 @@ func deleteCommand() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
disableUsage(cmd)
id := args[0]
s, err := state.Load(id)
if err != nil {
return err
}
running, err := jail.IsRunning(cmd.Context(), id, 0)
if err != nil {
return fmt.Errorf("delete: failed to determine if jail is running: %w", err)
Expand Down Expand Up @@ -63,7 +68,24 @@ func deleteCommand() *cobra.Command {
if err != nil {
return err
}
return state.Remove(id)
err = state.Remove(id)
if err != nil {
return err
}

if ociConfig.Hooks != nil {
for _, h := range ociConfig.Hooks.Poststop {
output := s.Output()
output.Annotations = ociConfig.Annotations
err = hook.Run(&output, &h)
if err != nil {
return err
}

}
}

return nil
},
}
}
32 changes: 1 addition & 31 deletions cmd/runj/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"

"go.sbk.wtf/runj/jail"
"go.sbk.wtf/runj/runtimespec"
"go.sbk.wtf/runj/state"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -77,14 +76,7 @@ func stateCommand() *cobra.Command {
}
}
}
output := StateOutput{
OCIVersion: runtimespec.Version,
ID: id,
Status: string(s.Status),
PID: s.PID,
Bundle: s.Bundle,
}
b, err := json.MarshalIndent(output, "", " ")
b, err := json.MarshalIndent(s.Output(), "", " ")
if err != nil {
return err
}
Expand All @@ -93,25 +85,3 @@ func stateCommand() *cobra.Command {
},
}
}

// StateOutput is the expected output format for the state command
/*
{
"ociVersion": "0.2.0",
"id": "oci-container1",
"status": "running",
"pid": 4422,
"bundle": "/containers/redis",
"annotations": {
"myKey": "myValue"
}
}
*/
type StateOutput struct {
OCIVersion string `json:"ociVersion"`
ID string `json:"id"`
Status string `json:"status"`
PID int `json:"pid,omitempty"`
Bundle string `json:"bundle"`
Annotations map[string]string `json:"annotations,omitempty"`
}
43 changes: 43 additions & 0 deletions hook/hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package hook

import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"time"

"go.sbk.wtf/runj/runtimespec"
"go.sbk.wtf/runj/state"
)

// Run runs a given hook
func Run(s *state.Output, h *runtimespec.Hook) error {
b, err := json.Marshal(s)
if err != nil {
return err
}
var stdout, stderr bytes.Buffer
var cancelFunc context.CancelFunc
ctx := context.Background()

if h.Timeout != nil {
ctx, cancelFunc = context.WithTimeout(ctx, time.Duration(*h.Timeout)*time.Second)
defer cancelFunc()
}

cmd := exec.CommandContext(ctx, h.Path, h.Args[1:]...)
cmd.Env = h.Env
cmd.Stdin = bytes.NewReader(b)
cmd.Stdout = &stdout
cmd.Stderr = &stderr

err = cmd.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "error running hook: %v, stdout: %s, stderr: %s\n", err, stdout.String(), stderr.String())
}

return err
}
54 changes: 44 additions & 10 deletions runtimespec/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,11 @@ type Spec struct {
// Mounts configures additional mounts (on top of Root).
Mounts []Mount `json:"mounts,omitempty"`

// Modification by Samuel Karp
/*
// Hooks configures callbacks for container lifecycle events.
Hooks *Hooks `json:"hooks,omitempty" platform:"linux,solaris"`
// Annotations contains arbitrary metadata for the container.
Annotations map[string]string `json:"annotations,omitempty"`
// End of modification
*/
// Hooks configures callbacks for container lifecycle events.
Hooks *Hooks `json:"hooks,omitempty"`

// Annotations contains arbitrary metadata for the container.
Annotations map[string]string `json:"annotations,omitempty"`

// Modification by Samuel Karp
FreeBSD *FreeBSD `json:"freebsd,omitempty"`
Expand Down Expand Up @@ -143,6 +140,45 @@ type Mount struct {
Options []string `json:"options,omitempty"`
}

// Hook specifies a command that is run at a particular event in the lifecycle of a container
type Hook struct {
Path string `json:"path"`
Args []string `json:"args,omitempty"`
Env []string `json:"env,omitempty"`
Timeout *int `json:"timeout,omitempty"`
}

// Hooks specifies a command that is run in the container at a particular event in the lifecycle of a container
// Hooks for container setup and teardown
type Hooks struct {
// Modification by Artem Khramov
/*
// Prestart is Deprecated. Prestart is a list of hooks to be run before the container process is executed.
// It is called in the Runtime Namespace
Prestart []Hook `json:"prestart,omitempty"`
*/
// End of modification
// CreateRuntime is a list of hooks to be run after the container has been created but before pivot_root or any equivalent operation has been called
// It is called in the Runtime Namespace
CreateRuntime []Hook `json:"createRuntime,omitempty"`
// Modification by Artem Khramov
/*
// CreateContainer is a list of hooks to be run after the container has been created but before pivot_root or any equivalent operation has been called
// It is called in the Container Namespace
CreateContainer []Hook `json:"createContainer,omitempty"`
// StartContainer is a list of hooks to be run after the start operation is called but before the container process is started
// It is called in the Container Namespace
StartContainer []Hook `json:"startContainer,omitempty"`
// Poststart is a list of hooks to be run after the container process is started.
// It is called in the Runtime Namespace
Poststart []Hook `json:"poststart,omitempty"`
*/
// End of modification
// Poststop is a list of hooks to be run after the container process exits.
// It is called in the Runtime Namespace
Poststop []Hook `json:"poststop,omitempty"`
}

// Modification by Samuel Karp

// FreeBSD specifies FreeBSD-specific configuration options
Expand Down Expand Up @@ -202,8 +238,6 @@ type FreeBSDVNetMode string
// Modification by Samuel Karp
/*
Omitted type definitions for:
Hook
Hooks
Linux
LinuxNamespace
LinuxNamespaceType
Expand Down
35 changes: 35 additions & 0 deletions state/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/json"
"os"
"path/filepath"

"go.sbk.wtf/runj/runtimespec"
)

const stateFile = "state.json"
Expand Down Expand Up @@ -36,6 +38,39 @@ type State struct {
PID int
}

// Output is the expected output format for the state command
/*
{
"ociVersion": "0.2.0",
"id": "oci-container1",
"status": "running",
"pid": 4422,
"bundle": "/containers/redis",
"annotations": {
"myKey": "myValue"
}
}
*/
type Output struct {
OCIVersion string `json:"ociVersion"`
ID string `json:"id"`
Status string `json:"status"`
PID int `json:"pid,omitempty"`
Bundle string `json:"bundle"`
Annotations map[string]string `json:"annotations,omitempty"`
}

// Output converts the state to the "Output" format expected by hooks
func (s *State) Output() Output {
return Output{
OCIVersion: runtimespec.Version,
ID: s.ID,
Status: string(s.Status),
PID: s.PID,
Bundle: s.Bundle,
}
}

// Load reads the state from disk and parses it
func Load(id string) (*State, error) {
d, err := os.ReadFile(filepath.Join(Dir(id), stateFile))
Expand Down
64 changes: 64 additions & 0 deletions test/integration/integ_hooks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//go:build integration
// +build integration

package integration

import (
"os"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/assert"
"go.sbk.wtf/runj/runtimespec"
)

func TestHooks(t *testing.T) {
spec := setupSimpleExitingJail(t)
dir := t.TempDir()

spec.Process = &runtimespec.Process{
Args: []string{"/integ-inside", "-test.v", "-test.run", "TestHello"},
}

spec.Hooks = &runtimespec.Hooks{
CreateRuntime: []runtimespec.Hook{runtimespec.Hook{
Path: "/usr/bin/touch",
Args: []string{"/usr/bin/touch", filepath.Join(dir, "create-runtime")},
}},
Poststop: []runtimespec.Hook{runtimespec.Hook{
Path: "/usr/bin/touch",
Args: []string{"/usr/bin/touch", filepath.Join(dir, "poststop")},
}},
}

_, _, err := runExitingJail(t, "integ-test-hooks", spec, 500*time.Millisecond)
assert.NoError(t, err)

_, err = os.Stat(filepath.Join(dir, "create-runtime"))
assert.NoError(t, err)
_, err = os.Stat(filepath.Join(dir, "poststop"))
assert.NoError(t, err)
}

func TestHookTimeout(t *testing.T) {
start := time.Now()
spec := setupSimpleExitingJail(t)
timeout := 4

spec.Process = &runtimespec.Process{
Args: []string{"/integ-inside", "-test.v", "-test.run", "TestHello"},
}

spec.Hooks = &runtimespec.Hooks{
CreateRuntime: []runtimespec.Hook{runtimespec.Hook{
Path: "/bin/sleep",
Args: []string{"/bin/sleep", "5000"},
Timeout: &timeout,
}},
}

_, _, err := runExitingJail(t, "integ-test-hooks", spec, 500*time.Millisecond)
assert.Error(t, err)
assert.Less(t, time.Duration(timeout)*time.Second, time.Since(start)*time.Second)
}