aboutsummaryrefslogtreecommitdiff
path: root/pkg/hooks/exec
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/hooks/exec')
-rw-r--r--pkg/hooks/exec/exec.go7
-rw-r--r--pkg/hooks/exec/exec_test.go6
-rw-r--r--pkg/hooks/exec/runtimeconfigfilter.go68
-rw-r--r--pkg/hooks/exec/runtimeconfigfilter_test.go266
4 files changed, 343 insertions, 4 deletions
diff --git a/pkg/hooks/exec/exec.go b/pkg/hooks/exec/exec.go
index 94469b1d2..0dd091561 100644
--- a/pkg/hooks/exec/exec.go
+++ b/pkg/hooks/exec/exec.go
@@ -10,6 +10,7 @@ import (
"time"
rspec "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/pkg/errors"
)
// DefaultPostKillTimeout is the recommended default post-kill timeout.
@@ -42,7 +43,11 @@ func Run(ctx context.Context, hook *rspec.Hook, state []byte, stdout io.Writer,
}
exit := make(chan error, 1)
go func() {
- exit <- cmd.Wait()
+ err := cmd.Wait()
+ if err != nil {
+ err = errors.Wrapf(err, "executing %v", cmd.Args)
+ }
+ exit <- err
}()
select {
diff --git a/pkg/hooks/exec/exec_test.go b/pkg/hooks/exec/exec_test.go
index 62e45ff3a..7aac315cb 100644
--- a/pkg/hooks/exec/exec_test.go
+++ b/pkg/hooks/exec/exec_test.go
@@ -163,14 +163,14 @@ func TestRunCancel(t *testing.T) {
name: "context timeout",
contextTimeout: time.Duration(1) * time.Second,
expectedStdout: "waiting\n",
- expectedHookError: "^signal: killed$",
+ expectedHookError: "^executing \\[sh -c echo waiting; sleep 2; echo done]: signal: killed$",
expectedRunError: context.DeadlineExceeded,
},
{
name: "hook timeout",
hookTimeout: &one,
expectedStdout: "waiting\n",
- expectedHookError: "^signal: killed$",
+ expectedHookError: "^executing \\[sh -c echo waiting; sleep 2; echo done]: signal: killed$",
expectedRunError: context.DeadlineExceeded,
},
} {
@@ -207,7 +207,7 @@ func TestRunKillTimeout(t *testing.T) {
}
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|signal: killed)$", hookErr)
+ assert.Regexp(t, "^(failed to reap process within 0s of the kill signal|executing \\[sh -c sleep 1]: signal: killed)$", hookErr)
}
func init() {
diff --git a/pkg/hooks/exec/runtimeconfigfilter.go b/pkg/hooks/exec/runtimeconfigfilter.go
new file mode 100644
index 000000000..c6971f680
--- /dev/null
+++ b/pkg/hooks/exec/runtimeconfigfilter.go
@@ -0,0 +1,68 @@
+package exec
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "reflect"
+ "time"
+
+ "github.com/davecgh/go-spew/spew"
+ spec "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/pkg/errors"
+ "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)
+ for i, hook := range hooks {
+ 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, errors.Wrapf(err, "unmarshal output from config-filter hook %d", i)
+ }
+
+ if !reflect.DeepEqual(config, &newConfig) {
+ old := spewConfig.Sdump(config)
+ new := spewConfig.Sdump(&newConfig)
+ diff, err := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
+ A: difflib.SplitLines(old),
+ B: difflib.SplitLines(new),
+ 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
new file mode 100644
index 000000000..52d590d14
--- /dev/null
+++ b/pkg/hooks/exec/runtimeconfigfilter_test.go
@@ -0,0 +1,266 @@
+package exec
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "testing"
+ "time"
+
+ spec "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/pkg/errors"
+ "github.com/stretchr/testify/assert"
+)
+
+func pointerInt(value int) *int {
+ return &value
+}
+
+func pointerUInt32(value uint32) *uint32 {
+ return &value
+}
+
+func pointerFileMode(value os.FileMode) *os.FileMode {
+ return &value
+}
+
+func TestRuntimeConfigFilter(t *testing.T) {
+ unexpectedEndOfJSONInput := json.Unmarshal([]byte("{\n"), nil)
+
+ for _, test := range []struct {
+ name string
+ contextTimeout time.Duration
+ hooks []spec.Hook
+ input *spec.Spec
+ expected *spec.Spec
+ expectedHookError string
+ expectedRunError error
+ }{
+ {
+ 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: pointerFileMode(0600),
+ UID: pointerUInt32(0),
+ GID: pointerUInt32(0),
+ },
+ },
+ },
+ },
+ 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: pointerFileMode(0600),
+ UID: pointerUInt32(0),
+ GID: pointerUInt32(0),
+ },
+ {
+ Path: "/dev/sda",
+ Type: "b",
+ Major: 8,
+ Minor: 0,
+ FileMode: pointerFileMode(0600),
+ UID: pointerUInt32(0),
+ GID: pointerUInt32(0),
+ },
+ },
+ },
+ },
+ },
+ {
+ 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: pointerFileMode(0600),
+ UID: pointerUInt32(0),
+ GID: pointerUInt32(0),
+ },
+ },
+ },
+ },
+ 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: pointerFileMode(0600),
+ UID: pointerUInt32(0),
+ GID: pointerUInt32(0),
+ },
+ {
+ Path: "/dev/sdb",
+ Type: "b",
+ Major: 8,
+ Minor: 0,
+ FileMode: pointerFileMode(0600),
+ UID: pointerUInt32(0),
+ GID: pointerUInt32(0),
+ },
+ },
+ },
+ },
+ },
+ {
+ 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: pointerInt(1),
+ },
+ },
+ 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,
+ },
+ } {
+ 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)
+ assert.Equal(t, test.expectedRunError, errors.Cause(err))
+ if test.expectedHookError == "" {
+ if hookErr != nil {
+ t.Fatal(hookErr)
+ }
+ } else {
+ assert.Regexp(t, test.expectedHookError, hookErr.Error())
+ }
+ assert.Equal(t, test.expected, test.input)
+ })
+ }
+}