Files
podman/pkg/hooks/exec/exec_test.go
W. Trevor King 99e642d940 pkg/hooks/exec: Include failed command in hook errors
For example:

  $ cat /etc/containers/oci/hooks.d/test.json
  {
    "version": "1.0.0",
    "hook": {
      "path": "/bin/sh",
      "args": ["sh", "-c", "echo 'oh, noes!' >&2; exit 1"]
    },
    "when": {
      "always": true
    },
    "stages": ["precreate"]
  }
  $ podman run --rm docker.io/library/alpine echo 'successful container'
  error setting up OCI Hooks: executing [sh -c echo 'oh, noes!' >&2; exit 1]: exit status 1

The rendered command isn't in in the right syntax for copy/pasting
into a shell, but it should be enough for the user to be able to
locate the failing hook.  They'll need to know their hook directories,
but with the previous commits requiring explicit hook directories it's
more likely that the caller is aware of them.  And if they run at a
debug level, they can see the lookups in the logs:

  $ podman --log-level=debug --hooks-dir=/etc/containers/oci/hooks.d run --rm docker.io/library/alpine echo 'successful container' 2>&1 | grep -i hook
  time="2018-12-02T22:15:16-08:00" level=debug msg="reading hooks from /etc/containers/oci/hooks.d"
  time="2018-12-02T22:15:16-08:00" level=debug msg="added hook /etc/containers/oci/hooks.d/test.json"
  time="2018-12-02T22:15:16-08:00" level=debug msg="hook test.json matched; adding to stages [precreate]"
  time="2018-12-02T22:15:16-08:00" level=warning msg="container 3695c6ba0cc961918bd3e4a769c52bd08b82afea5cd79e9749e9c7a63b5e7100: precreate hook: executing [sh -c echo 'oh, noes!' >&2; exit 1]: exit status 1"
  time="2018-12-02T22:15:16-08:00" level=error msg="error setting up OCI Hooks: executing [sh -c echo 'oh, noes!' >&2; exit 1]: exit status 1"

Signed-off-by: W. Trevor King <wking@tremily.us>
2019-01-08 21:06:17 -08:00

221 lines
5.4 KiB
Go

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 _, test := 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",
},
},
} {
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 _, test := 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,
},
} {
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")
}
}