mirror of
https://github.com/containers/podman.git
synced 2025-05-24 02:27:00 +08:00

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>
221 lines
5.4 KiB
Go
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")
|
|
}
|
|
}
|