summaryrefslogtreecommitdiff
path: root/pkg/hooks/exec
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/hooks/exec')
-rw-r--r--pkg/hooks/exec/exec.go69
-rw-r--r--pkg/hooks/exec/exec_test.go222
-rw-r--r--pkg/hooks/exec/runtimeconfigfilter.go72
-rw-r--r--pkg/hooks/exec/runtimeconfigfilter_test.go265
4 files changed, 0 insertions, 628 deletions
diff --git a/pkg/hooks/exec/exec.go b/pkg/hooks/exec/exec.go
deleted file mode 100644
index bc639245f..000000000
--- a/pkg/hooks/exec/exec.go
+++ /dev/null
@@ -1,69 +0,0 @@
-// Package exec provides utilities for executing Open Container Initiative runtime hooks.
-package exec
-
-import (
- "bytes"
- "context"
- "fmt"
- "io"
- osexec "os/exec"
- "time"
-
- rspec "github.com/opencontainers/runtime-spec/specs-go"
- "github.com/sirupsen/logrus"
-)
-
-// DefaultPostKillTimeout is the recommended default post-kill timeout.
-const DefaultPostKillTimeout = time.Duration(10) * time.Second
-
-// Run executes the hook and waits for it to complete or for the
-// context or hook-specified timeout to expire.
-func Run(ctx context.Context, hook *rspec.Hook, state []byte, stdout io.Writer, stderr io.Writer, postKillTimeout time.Duration) (hookErr, err error) {
- cmd := osexec.Cmd{
- Path: hook.Path,
- Args: hook.Args,
- Env: hook.Env,
- Stdin: bytes.NewReader(state),
- Stdout: stdout,
- Stderr: stderr,
- }
- if cmd.Env == nil {
- cmd.Env = []string{}
- }
-
- if hook.Timeout != nil {
- var cancel context.CancelFunc
- ctx, cancel = context.WithTimeout(ctx, time.Duration(*hook.Timeout)*time.Second)
- defer cancel()
- }
-
- err = cmd.Start()
- if err != nil {
- return err, err
- }
- exit := make(chan error, 1)
- go func() {
- err := cmd.Wait()
- if err != nil {
- err = fmt.Errorf("executing %v: %w", cmd.Args, err)
- }
- exit <- err
- }()
-
- select {
- case err = <-exit:
- return err, err
- case <-ctx.Done():
- if err := cmd.Process.Kill(); err != nil {
- logrus.Errorf("Failed to kill pid %v", cmd.Process)
- }
- timer := time.NewTimer(postKillTimeout)
- defer timer.Stop()
- select {
- case <-timer.C:
- err = fmt.Errorf("failed to reap process within %s of the kill signal", postKillTimeout)
- case err = <-exit:
- }
- return err, ctx.Err()
- }
-}
diff --git a/pkg/hooks/exec/exec_test.go b/pkg/hooks/exec/exec_test.go
deleted file mode 100644
index 1e105373d..000000000
--- a/pkg/hooks/exec/exec_test.go
+++ /dev/null
@@ -1,222 +0,0 @@
-package exec
-
-import (
- "bytes"
- "context"
- "fmt"
- "os"
- "runtime"
- "strings"
- "testing"
- "time"
-
- rspec "github.com/opencontainers/runtime-spec/specs-go"
- "github.com/stretchr/testify/assert"
-)
-
-// path is the path to an example hook executable.
-var path string
-
-// unavoidableEnvironmentKeys may be injected even if the hook
-// executable is executed with a requested empty environment.
-var unavoidableEnvironmentKeys []string
-
-func TestRun(t *testing.T) {
- ctx := context.Background()
- hook := &rspec.Hook{
- Path: path,
- Args: []string{"sh", "-c", "cat"},
- }
- var stderr, stdout bytes.Buffer
- hookErr, err := Run(ctx, hook, []byte("{}"), &stdout, &stderr, DefaultPostKillTimeout)
- if err != nil {
- t.Fatal(err)
- }
- if hookErr != nil {
- t.Fatal(hookErr)
- }
- assert.Equal(t, "{}", stdout.String())
- assert.Equal(t, "", stderr.String())
-}
-
-func TestRunIgnoreOutput(t *testing.T) {
- ctx := context.Background()
- hook := &rspec.Hook{
- Path: path,
- Args: []string{"sh", "-c", "cat"},
- }
- hookErr, err := Run(ctx, hook, []byte("{}"), nil, nil, DefaultPostKillTimeout)
- if err != nil {
- t.Fatal(err)
- }
- if hookErr != nil {
- t.Fatal(hookErr)
- }
-}
-
-func TestRunFailedStart(t *testing.T) {
- ctx := context.Background()
- hook := &rspec.Hook{
- Path: "/does/not/exist",
- }
- hookErr, err := Run(ctx, hook, []byte("{}"), nil, nil, DefaultPostKillTimeout)
- if err == nil {
- t.Fatal("unexpected success")
- }
- if !os.IsNotExist(err) {
- t.Fatal(err)
- }
- assert.Equal(t, err, hookErr)
-}
-
-func parseEnvironment(input string) (env map[string]string, err error) {
- env = map[string]string{}
- lines := strings.Split(input, "\n")
- for i, line := range lines {
- if line == "" && i == len(lines)-1 {
- continue // no content after the terminal newline
- }
- keyValue := strings.SplitN(line, "=", 2)
- if len(keyValue) < 2 {
- return env, fmt.Errorf("no = in environment line: %q", line)
- }
- env[keyValue[0]] = keyValue[1]
- }
- for _, key := range unavoidableEnvironmentKeys {
- delete(env, key)
- }
- return env, nil
-}
-
-func TestRunEnvironment(t *testing.T) {
- ctx := context.Background()
- hook := &rspec.Hook{
- Path: path,
- Args: []string{"sh", "-c", "env"},
- }
- for _, tt := range []struct {
- name string
- env []string
- expected map[string]string
- }{
- {
- name: "unset",
- expected: map[string]string{},
- },
- {
- name: "set empty",
- env: []string{},
- expected: map[string]string{},
- },
- {
- name: "set",
- env: []string{
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
- "TERM=xterm",
- },
- expected: map[string]string{
- "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
- "TERM": "xterm",
- },
- },
- } {
- test := tt
- t.Run(test.name, func(t *testing.T) {
- var stderr, stdout bytes.Buffer
- hook.Env = test.env
- hookErr, err := Run(ctx, hook, []byte("{}"), &stdout, &stderr, DefaultPostKillTimeout)
- if err != nil {
- t.Fatal(err)
- }
- if hookErr != nil {
- t.Fatal(hookErr)
- }
- assert.Equal(t, "", stderr.String())
-
- env, err := parseEnvironment(stdout.String())
- if err != nil {
- t.Fatal(err)
- }
- assert.Equal(t, test.expected, env)
- })
- }
-}
-
-func TestRunCancel(t *testing.T) {
- hook := &rspec.Hook{
- Path: path,
- Args: []string{"sh", "-c", "echo waiting; sleep 2; echo done"},
- }
- one := 1
- for _, tt := range []struct {
- name string
- contextTimeout time.Duration
- hookTimeout *int
- expectedHookError string
- expectedRunError error
- expectedStdout string
- }{
- {
- name: "no timeouts",
- expectedStdout: "waiting\ndone\n",
- },
- {
- name: "context timeout",
- contextTimeout: time.Duration(1) * time.Second,
- expectedStdout: "waiting\n",
- expectedHookError: "^executing \\[sh -c echo waiting; sleep 2; echo done]: signal: killed$",
- expectedRunError: context.DeadlineExceeded,
- },
- {
- name: "hook timeout",
- hookTimeout: &one,
- expectedStdout: "waiting\n",
- expectedHookError: "^executing \\[sh -c echo waiting; sleep 2; echo done]: signal: killed$",
- expectedRunError: context.DeadlineExceeded,
- },
- } {
- test := tt
- t.Run(test.name, func(t *testing.T) {
- ctx := context.Background()
- var stderr, stdout bytes.Buffer
- if test.contextTimeout > 0 {
- var cancel context.CancelFunc
- ctx, cancel = context.WithTimeout(ctx, test.contextTimeout)
- defer cancel()
- }
- hook.Timeout = test.hookTimeout
- hookErr, err := Run(ctx, hook, []byte("{}"), &stdout, &stderr, DefaultPostKillTimeout)
- assert.Equal(t, test.expectedRunError, err)
- if test.expectedHookError == "" {
- if hookErr != nil {
- t.Fatal(hookErr)
- }
- } else {
- assert.Regexp(t, test.expectedHookError, hookErr.Error())
- }
- assert.Equal(t, "", stderr.String())
- assert.Equal(t, test.expectedStdout, stdout.String())
- })
- }
-}
-
-func TestRunKillTimeout(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), time.Duration(500)*time.Millisecond)
- defer cancel()
- hook := &rspec.Hook{
- Path: path,
- Args: []string{"sh", "-c", "sleep 1"},
- }
- hookErr, err := Run(ctx, hook, []byte("{}"), nil, nil, time.Duration(0))
- assert.Equal(t, context.DeadlineExceeded, err)
- assert.Regexp(t, "^(failed to reap process within 0s of the kill signal|executing \\[sh -c sleep 1]: signal: killed)$", hookErr)
-}
-
-func init() {
- if runtime.GOOS != "windows" {
- path = "/bin/sh"
- unavoidableEnvironmentKeys = []string{"PWD", "SHLVL", "_"}
- } else {
- panic("we need a reliable executable path on Windows")
- }
-}
diff --git a/pkg/hooks/exec/runtimeconfigfilter.go b/pkg/hooks/exec/runtimeconfigfilter.go
deleted file mode 100644
index 72d4b8979..000000000
--- a/pkg/hooks/exec/runtimeconfigfilter.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package exec
-
-import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "reflect"
- "time"
-
- "github.com/davecgh/go-spew/spew"
- spec "github.com/opencontainers/runtime-spec/specs-go"
- "github.com/pmezard/go-difflib/difflib"
- "github.com/sirupsen/logrus"
-)
-
-var spewConfig = spew.ConfigState{
- Indent: " ",
- DisablePointerAddresses: true,
- DisableCapacities: true,
- SortKeys: true,
-}
-
-// RuntimeConfigFilter calls a series of hooks. But instead of
-// passing container state on their standard input,
-// RuntimeConfigFilter passes the proposed runtime configuration (and
-// reads back a possibly-altered form from their standard output).
-func RuntimeConfigFilter(ctx context.Context, hooks []spec.Hook, config *spec.Spec, postKillTimeout time.Duration) (hookErr, err error) {
- data, err := json.Marshal(config)
- if err != nil {
- return nil, err
- }
- for i, hook := range hooks {
- hook := hook
- var stdout bytes.Buffer
- hookErr, err = Run(ctx, &hook, data, &stdout, nil, postKillTimeout)
- if err != nil {
- return hookErr, err
- }
-
- data = stdout.Bytes()
- var newConfig spec.Spec
- err = json.Unmarshal(data, &newConfig)
- if err != nil {
- logrus.Debugf("invalid JSON from config-filter hook %d:\n%s", i, string(data))
- return nil, fmt.Errorf("unmarshal output from config-filter hook %d: %w", i, err)
- }
-
- if !reflect.DeepEqual(config, &newConfig) {
- oldConfig := spewConfig.Sdump(config)
- newConfig := spewConfig.Sdump(&newConfig)
- diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
- A: difflib.SplitLines(oldConfig),
- B: difflib.SplitLines(newConfig),
- FromFile: "Old",
- FromDate: "",
- ToFile: "New",
- ToDate: "",
- Context: 1,
- })
- if err == nil {
- logrus.Debugf("precreate hook %d made configuration changes:\n%s", i, diff)
- } else {
- logrus.Warnf("Precreate hook %d made configuration changes, but we could not compute a diff: %v", i, err)
- }
- }
-
- *config = newConfig
- }
-
- return nil, nil
-}
diff --git a/pkg/hooks/exec/runtimeconfigfilter_test.go b/pkg/hooks/exec/runtimeconfigfilter_test.go
deleted file mode 100644
index a4e9b1fdb..000000000
--- a/pkg/hooks/exec/runtimeconfigfilter_test.go
+++ /dev/null
@@ -1,265 +0,0 @@
-package exec
-
-import (
- "context"
- "encoding/json"
- "errors"
- "os"
- "testing"
- "time"
-
- spec "github.com/opencontainers/runtime-spec/specs-go"
- "github.com/stretchr/testify/assert"
-)
-
-func TestRuntimeConfigFilter(t *testing.T) {
- unexpectedEndOfJSONInput := json.Unmarshal([]byte("{\n"), nil) //nolint:govet // this should force the error
- fileMode := os.FileMode(0600)
- rootUint32 := uint32(0)
- binUser := int(1)
- for _, tt := range []struct {
- name string
- contextTimeout time.Duration
- hooks []spec.Hook
- input *spec.Spec
- expected *spec.Spec
- expectedHookError string
- expectedRunError error
- expectedRunErrorString string
- }{
- {
- name: "no-op",
- hooks: []spec.Hook{
- {
- Path: path,
- Args: []string{"sh", "-c", "cat"},
- },
- },
- input: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- },
- expected: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- },
- },
- {
- name: "device injection",
- hooks: []spec.Hook{
- {
- Path: path,
- Args: []string{"sh", "-c", `sed 's|\("gid":0}\)|\1,{"path": "/dev/sda","type":"b","major":8,"minor":0,"fileMode":384,"uid":0,"gid":0}|'`},
- },
- },
- input: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- Linux: &spec.Linux{
- Devices: []spec.LinuxDevice{
- {
- Path: "/dev/fuse",
- Type: "c",
- Major: 10,
- Minor: 229,
- FileMode: &fileMode,
- UID: &rootUint32,
- GID: &rootUint32,
- },
- },
- },
- },
- expected: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- Linux: &spec.Linux{
- Devices: []spec.LinuxDevice{
- {
- Path: "/dev/fuse",
- Type: "c",
- Major: 10,
- Minor: 229,
- FileMode: &fileMode,
- UID: &rootUint32,
- GID: &rootUint32,
- },
- {
- Path: "/dev/sda",
- Type: "b",
- Major: 8,
- Minor: 0,
- FileMode: &fileMode,
- UID: &rootUint32,
- GID: &rootUint32,
- },
- },
- },
- },
- },
- {
- name: "chaining",
- hooks: []spec.Hook{
- {
- Path: path,
- Args: []string{"sh", "-c", `sed 's|\("gid":0}\)|\1,{"path": "/dev/sda","type":"b","major":8,"minor":0,"fileMode":384,"uid":0,"gid":0}|'`},
- },
- {
- Path: path,
- Args: []string{"sh", "-c", `sed 's|/dev/sda|/dev/sdb|'`},
- },
- },
- input: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- Linux: &spec.Linux{
- Devices: []spec.LinuxDevice{
- {
- Path: "/dev/fuse",
- Type: "c",
- Major: 10,
- Minor: 229,
- FileMode: &fileMode,
- UID: &rootUint32,
- GID: &rootUint32,
- },
- },
- },
- },
- expected: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- Linux: &spec.Linux{
- Devices: []spec.LinuxDevice{
- {
- Path: "/dev/fuse",
- Type: "c",
- Major: 10,
- Minor: 229,
- FileMode: &fileMode,
- UID: &rootUint32,
- GID: &rootUint32,
- },
- {
- Path: "/dev/sdb",
- Type: "b",
- Major: 8,
- Minor: 0,
- FileMode: &fileMode,
- UID: &rootUint32,
- GID: &rootUint32,
- },
- },
- },
- },
- },
- {
- name: "context timeout",
- contextTimeout: time.Duration(1) * time.Second,
- hooks: []spec.Hook{
- {
- Path: path,
- Args: []string{"sh", "-c", "sleep 2"},
- },
- },
- input: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- },
- expected: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- },
- expectedHookError: "^executing \\[sh -c sleep 2]: signal: killed$",
- expectedRunError: context.DeadlineExceeded,
- },
- {
- name: "hook timeout",
- hooks: []spec.Hook{
- {
- Path: path,
- Args: []string{"sh", "-c", "sleep 2"},
- Timeout: &binUser,
- },
- },
- input: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- },
- expected: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- },
- expectedHookError: "^executing \\[sh -c sleep 2]: signal: killed$",
- expectedRunError: context.DeadlineExceeded,
- },
- {
- name: "invalid JSON",
- hooks: []spec.Hook{
- {
- Path: path,
- Args: []string{"sh", "-c", "echo '{'"},
- },
- },
- input: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- },
- expected: &spec.Spec{
- Version: "1.0.0",
- Root: &spec.Root{
- Path: "rootfs",
- },
- },
- expectedRunError: unexpectedEndOfJSONInput,
- expectedRunErrorString: unexpectedEndOfJSONInput.Error(),
- },
- } {
- test := tt
- t.Run(test.name, func(t *testing.T) {
- ctx := context.Background()
- if test.contextTimeout > 0 {
- var cancel context.CancelFunc
- ctx, cancel = context.WithTimeout(ctx, test.contextTimeout)
- defer cancel()
- }
- hookErr, err := RuntimeConfigFilter(ctx, test.hooks, test.input, DefaultPostKillTimeout)
- if test.expectedRunError != nil {
- if test.expectedRunErrorString != "" {
- assert.Contains(t, err.Error(), test.expectedRunErrorString)
- } else {
- assert.True(t, errors.Is(err, test.expectedRunError))
- }
- }
- if test.expectedHookError == "" {
- if hookErr != nil {
- t.Fatal(hookErr)
- }
- } else {
- assert.Regexp(t, test.expectedHookError, hookErr.Error())
- }
- assert.Equal(t, test.expected, test.input)
- })
- }
-}