diff options
Diffstat (limited to 'pkg/hooks/exec')
-rw-r--r-- | pkg/hooks/exec/exec.go | 69 | ||||
-rw-r--r-- | pkg/hooks/exec/exec_test.go | 222 | ||||
-rw-r--r-- | pkg/hooks/exec/runtimeconfigfilter.go | 72 | ||||
-rw-r--r-- | pkg/hooks/exec/runtimeconfigfilter_test.go | 265 |
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) - }) - } -} |