Files
kata-containers/cli/exec_test.go
Wei Zhang 2ed94cbd9d Config: Remove ConfigJSONKey from annotations
Fixes: #2023

We can get OCI spec config from bundle instead of annotations, so this
field isn't necessary.

Signed-off-by: Wei Zhang <weizhang555.zw@gmail.com>
2019-09-17 11:47:06 +08:00

788 lines
21 KiB
Go

// Copyright (c) 2017 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"context"
"flag"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"testing"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli"
vc "github.com/kata-containers/runtime/virtcontainers"
vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations"
"github.com/kata-containers/runtime/virtcontainers/pkg/compatoci"
"github.com/kata-containers/runtime/virtcontainers/pkg/vcmock"
"github.com/kata-containers/runtime/virtcontainers/types"
)
func TestExecCLIFunction(t *testing.T) {
assert := assert.New(t)
flagSet := &flag.FlagSet{}
ctx := createCLIContext(flagSet)
fn, ok := startCLICommand.Action.(func(context *cli.Context) error)
assert.True(ok)
// no container-id in the Metadata
err := fn(ctx)
assert.Error(err)
assert.False(vcmock.IsMockError(err))
path, err := createTempContainerIDMapping("xyz", "xyz")
assert.NoError(err)
defer os.RemoveAll(path)
// pass container-id
flagSet = flag.NewFlagSet("container-id", flag.ContinueOnError)
flagSet.Parse([]string{"xyz"})
ctx = createCLIContext(flagSet)
err = fn(ctx)
assert.Error(err)
assert.True(vcmock.IsMockError(err))
}
func TestExecuteErrors(t *testing.T) {
assert := assert.New(t)
flagSet := flag.NewFlagSet("", 0)
ctx := createCLIContext(flagSet)
// missing container id
err := execute(context.Background(), ctx)
assert.Error(err)
assert.False(vcmock.IsMockError(err))
path, err := createTempContainerIDMapping(testContainerID, testSandboxID)
assert.NoError(err)
defer os.RemoveAll(path)
// StatusSandbox error
flagSet.Parse([]string{testContainerID})
err = execute(context.Background(), ctx)
assert.Error(err)
assert.True(vcmock.IsMockError(err))
// Config path missing in annotations
annotations := map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
}
testingImpl.StatusContainerFunc = func(ctx context.Context, sandboxID, containerID string) (vc.ContainerStatus, error) {
return newSingleContainerStatus(testContainerID, types.ContainerState{}, annotations, &specs.Spec{Process: &specs.Process{}}), nil
}
defer func() {
testingImpl.StatusContainerFunc = nil
}()
err = execute(context.Background(), ctx)
assert.Error(err)
assert.False(vcmock.IsMockError(err))
// Container state undefined
rootPath, bundlePath := testConfigSetup(t)
defer os.RemoveAll(rootPath)
ociSpec, err := compatoci.ParseConfigJSON(bundlePath)
assert.NoError(err)
annotations = map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
}
containerState := types.ContainerState{}
testingImpl.StatusContainerFunc = func(ctx context.Context, sandboxID, containerID string) (vc.ContainerStatus, error) {
return newSingleContainerStatus(testContainerID, containerState, annotations, &ociSpec), nil
}
err = execute(context.Background(), ctx)
assert.Error(err)
assert.False(vcmock.IsMockError(err))
// Container paused
containerState = types.ContainerState{
State: types.StatePaused,
}
testingImpl.StatusContainerFunc = func(ctx context.Context, sandboxID, containerID string) (vc.ContainerStatus, error) {
return newSingleContainerStatus(testContainerID, containerState, annotations, &ociSpec), nil
}
err = execute(context.Background(), ctx)
assert.Error(err)
assert.False(vcmock.IsMockError(err))
// Container stopped
containerState = types.ContainerState{
State: types.StateStopped,
}
testingImpl.StatusContainerFunc = func(ctx context.Context, sandboxID, containerID string) (vc.ContainerStatus, error) {
return newSingleContainerStatus(testContainerID, containerState, annotations, &ociSpec), nil
}
err = execute(context.Background(), ctx)
assert.Error(err)
assert.False(vcmock.IsMockError(err))
}
func TestExecuteErrorReadingProcessJson(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir("", "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
// non-existent path
processPath := filepath.Join(tmpdir, "process.json")
flagSet := flag.NewFlagSet("", 0)
flagSet.String("process", processPath, "")
flagSet.Parse([]string{testContainerID})
ctx := createCLIContext(flagSet)
rootPath, bundlePath := testConfigSetup(t)
defer os.RemoveAll(rootPath)
ociSpec, err := compatoci.ParseConfigJSON(bundlePath)
assert.NoError(err)
annotations := map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
}
state := types.ContainerState{
State: types.StateRunning,
}
path, err := createTempContainerIDMapping(testContainerID, testSandboxID)
assert.NoError(err)
defer os.RemoveAll(path)
testingImpl.StatusContainerFunc = func(ctx context.Context, sandboxID, containerID string) (vc.ContainerStatus, error) {
return newSingleContainerStatus(testContainerID, state, annotations, &ociSpec), nil
}
defer func() {
testingImpl.StatusContainerFunc = nil
}()
// Note: flags can only be tested with the CLI command function
fn, ok := execCLICommand.Action.(func(context *cli.Context) error)
assert.True(ok)
err = fn(ctx)
assert.Error(err)
assert.False(vcmock.IsMockError(err))
}
func TestExecuteErrorOpeningConsole(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir("", "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
consoleSock := filepath.Join(tmpdir, "console-sock")
flagSet := flag.NewFlagSet("", 0)
flagSet.String("console-socket", consoleSock, "")
flagSet.Parse([]string{testContainerID})
ctx := createCLIContext(flagSet)
rootPath, bundlePath := testConfigSetup(t)
defer os.RemoveAll(rootPath)
ociSpec, err := compatoci.ParseConfigJSON(bundlePath)
assert.NoError(err)
annotations := map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
}
state := types.ContainerState{
State: types.StateRunning,
}
path, err := createTempContainerIDMapping(testContainerID, testSandboxID)
assert.NoError(err)
defer os.RemoveAll(path)
testingImpl.StatusContainerFunc = func(ctx context.Context, sandboxID, containerID string) (vc.ContainerStatus, error) {
return newSingleContainerStatus(testContainerID, state, annotations, &ociSpec), nil
}
defer func() {
testingImpl.StatusContainerFunc = nil
}()
// Note: flags can only be tested with the CLI command function
fn, ok := execCLICommand.Action.(func(context *cli.Context) error)
assert.True(ok)
err = fn(ctx)
assert.Error(err)
assert.False(vcmock.IsMockError(err))
}
func testExecParamsSetup(t *testing.T, pidFilePath, consolePath string, detach bool) *flag.FlagSet {
flagSet := flag.NewFlagSet("", 0)
flagSet.String("pid-file", pidFilePath, "")
flagSet.String("console", consolePath, "")
flagSet.String("console-socket", "", "")
flagSet.Bool("detach", detach, "")
flagSet.String("process-label", "testlabel", "")
flagSet.Bool("no-subreaper", false, "")
return flagSet
}
func TestExecuteWithFlags(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir("", "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
pidFilePath := filepath.Join(tmpdir, "pid")
consolePath := "/dev/ptmx"
flagSet := testExecParamsSetup(t, pidFilePath, consolePath, false)
flagSet.String("user", "root", "")
flagSet.String("cwd", "/home/root", "")
flagSet.String("apparmor", "/tmp/profile", "")
flagSet.Bool("no-new-privs", false, "")
flagSet.Parse([]string{testContainerID, "/tmp/foo"})
ctx := createCLIContext(flagSet)
rootPath, bundlePath := testConfigSetup(t)
defer os.RemoveAll(rootPath)
ociSpec, err := compatoci.ParseConfigJSON(bundlePath)
assert.NoError(err)
annotations := map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
}
state := types.ContainerState{
State: types.StateRunning,
}
path, err := createTempContainerIDMapping(testContainerID, testSandboxID)
assert.NoError(err)
defer os.RemoveAll(path)
testingImpl.StatusContainerFunc = func(ctx context.Context, sandboxID, containerID string) (vc.ContainerStatus, error) {
return newSingleContainerStatus(testContainerID, state, annotations, &ociSpec), nil
}
defer func() {
testingImpl.StatusContainerFunc = nil
}()
fn, ok := execCLICommand.Action.(func(context *cli.Context) error)
assert.True(ok)
// EnterContainer error
err = fn(ctx)
assert.Error(err)
assert.True(vcmock.IsMockError(err))
testingImpl.EnterContainerFunc = func(ctx context.Context, sandboxID, containerID string, cmd types.Cmd) (vc.VCSandbox, vc.VCContainer, *vc.Process, error) {
return &vcmock.Sandbox{}, &vcmock.Container{}, &vc.Process{}, nil
}
defer func() {
testingImpl.EnterContainerFunc = nil
os.Remove(pidFilePath)
}()
// Process not running error
err = fn(ctx)
assert.Error(err)
assert.False(vcmock.IsMockError(err))
os.Remove(pidFilePath)
// Process ran and exited successfully
testingImpl.EnterContainerFunc = func(ctx context.Context, sandboxID, containerID string, cmd types.Cmd) (vc.VCSandbox, vc.VCContainer, *vc.Process, error) {
// create a fake container process
workload := []string{"cat", "/dev/null"}
command := exec.Command(workload[0], workload[1:]...)
err := command.Start()
assert.NoError(err, "Unable to start process %v: %s", workload, err)
vcProcess := vc.Process{}
vcProcess.Pid = command.Process.Pid
return &vcmock.Sandbox{}, &vcmock.Container{}, &vcProcess, nil
}
defer func() {
testingImpl.EnterContainerFunc = nil
os.Remove(pidFilePath)
}()
// Should get an exit code when run in non-detached mode.
err = fn(ctx)
_, ok = err.(*cli.ExitError)
assert.True(ok, true, "Exit code not received for fake workload process")
}
func TestExecuteWithFlagsDetached(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir("", "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
pidFilePath := filepath.Join(tmpdir, "pid")
consolePath := "/dev/ptmx"
detach := true
flagSet := testExecParamsSetup(t, pidFilePath, consolePath, detach)
flagSet.Parse([]string{testContainerID, "/tmp/foo"})
ctx := createCLIContext(flagSet)
rootPath, bundlePath := testConfigSetup(t)
defer os.RemoveAll(rootPath)
ociSpec, err := compatoci.ParseConfigJSON(bundlePath)
assert.NoError(err)
annotations := map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
}
state := types.ContainerState{
State: types.StateRunning,
}
path, err := createTempContainerIDMapping(testContainerID, testSandboxID)
assert.NoError(err)
defer os.RemoveAll(path)
testingImpl.StatusContainerFunc = func(ctx context.Context, sandboxID, containerID string) (vc.ContainerStatus, error) {
return newSingleContainerStatus(testContainerID, state, annotations, &ociSpec), nil
}
defer func() {
testingImpl.StatusContainerFunc = nil
}()
testingImpl.EnterContainerFunc = func(ctx context.Context, sandboxID, containerID string, cmd types.Cmd) (vc.VCSandbox, vc.VCContainer, *vc.Process, error) {
// create a fake container process
workload := []string{"cat", "/dev/null"}
command := exec.Command(workload[0], workload[1:]...)
err := command.Start()
assert.NoError(err, "Unable to start process %v: %s", workload, err)
vcProcess := vc.Process{}
vcProcess.Pid = command.Process.Pid
return &vcmock.Sandbox{}, &vcmock.Container{}, &vcProcess, nil
}
defer func() {
testingImpl.EnterContainerFunc = nil
os.Remove(pidFilePath)
}()
fn, ok := execCLICommand.Action.(func(context *cli.Context) error)
assert.True(ok)
err = fn(ctx)
assert.NoError(err)
}
func TestExecuteWithInvalidProcessJson(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir("", "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
pidFilePath := filepath.Join(tmpdir, "pid")
consolePath := "/dev/ptmx"
detach := false
flagSet := testExecParamsSetup(t, pidFilePath, consolePath, detach)
processPath := filepath.Join(tmpdir, "process.json")
flagSet.String("process", processPath, "")
f, err := os.OpenFile(processPath, os.O_RDWR|os.O_CREATE, testFileMode)
assert.NoError(err)
// invalidate the JSON
_, err = f.WriteString("{")
assert.NoError(err)
f.Close()
defer os.Remove(processPath)
flagSet.Parse([]string{testContainerID})
ctx := createCLIContext(flagSet)
rootPath, bundlePath := testConfigSetup(t)
defer os.RemoveAll(rootPath)
ociSpec, err := compatoci.ParseConfigJSON(bundlePath)
assert.NoError(err)
annotations := map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
}
state := types.ContainerState{
State: types.StateRunning,
}
path, err := createTempContainerIDMapping(testContainerID, testSandboxID)
assert.NoError(err)
defer os.RemoveAll(path)
testingImpl.StatusContainerFunc = func(ctx context.Context, sandboxID, containerID string) (vc.ContainerStatus, error) {
return newSingleContainerStatus(testContainerID, state, annotations, &ociSpec), nil
}
defer func() {
testingImpl.StatusContainerFunc = nil
}()
fn, ok := execCLICommand.Action.(func(context *cli.Context) error)
assert.True(ok)
err = fn(ctx)
assert.Error(err)
assert.False(vcmock.IsMockError(err))
}
func TestExecuteWithValidProcessJson(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir("", "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
pidFilePath := filepath.Join(tmpdir, "pid")
consolePath := "/dev/ptmx"
flagSet := testExecParamsSetup(t, pidFilePath, consolePath, false)
processPath := filepath.Join(tmpdir, "process.json")
flagSet.String("process", processPath, "")
flagSet.Parse([]string{testContainerID, "/tmp/foo"})
ctx := createCLIContext(flagSet)
rootPath, bundlePath := testConfigSetup(t)
defer os.RemoveAll(rootPath)
ociSpec, err := compatoci.ParseConfigJSON(bundlePath)
assert.NoError(err)
annotations := map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodContainer),
}
state := types.ContainerState{
State: types.StateRunning,
}
path, err := createTempContainerIDMapping(testContainerID, testSandboxID)
assert.NoError(err)
defer os.RemoveAll(path)
testingImpl.StatusContainerFunc = func(ctx context.Context, sandboxID, containerID string) (vc.ContainerStatus, error) {
return newSingleContainerStatus(testContainerID, state, annotations, &ociSpec), nil
}
defer func() {
testingImpl.StatusContainerFunc = nil
}()
processJSON := `{
"consoleSize": {
"height": 15,
"width": 15
},
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": "/"
}`
f, err := os.OpenFile(processPath, os.O_RDWR|os.O_CREATE, testFileMode)
assert.NoError(err)
_, err = f.WriteString(processJSON)
assert.NoError(err)
f.Close()
defer os.Remove(processPath)
workload := []string{"cat", "/dev/null"}
testingImpl.EnterContainerFunc = func(ctx context.Context, sandboxID, containerID string, cmd types.Cmd) (vc.VCSandbox, vc.VCContainer, *vc.Process, error) {
// create a fake container process
command := exec.Command(workload[0], workload[1:]...)
err := command.Start()
assert.NoError(err, "Unable to start process %v: %s", workload, err)
vcProcess := vc.Process{}
vcProcess.Pid = command.Process.Pid
return &vcmock.Sandbox{}, &vcmock.Container{}, &vcProcess, nil
}
defer func() {
testingImpl.EnterContainerFunc = nil
os.Remove(pidFilePath)
}()
fn, ok := execCLICommand.Action.(func(context *cli.Context) error)
assert.True(ok)
err = fn(ctx)
exitErr, ok := err.(*cli.ExitError)
assert.True(ok, true, "Exit code not received for fake workload process")
assert.Equal(exitErr.ExitCode(), 0, "Exit code should have been 0 for fake workload %s", workload)
}
func TestExecuteWithEmptyEnvironmentValue(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir("", "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
pidFilePath := filepath.Join(tmpdir, "pid")
consolePath := "/dev/ptmx"
flagSet := testExecParamsSetup(t, pidFilePath, consolePath, false)
processPath := filepath.Join(tmpdir, "process.json")
flagSet.String("process", processPath, "")
flagSet.Parse([]string{testContainerID})
ctx := createCLIContext(flagSet)
rootPath, bundlePath := testConfigSetup(t)
defer os.RemoveAll(rootPath)
ociSpec, err := compatoci.ParseConfigJSON(bundlePath)
assert.NoError(err)
annotations := map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodContainer),
}
state := types.ContainerState{
State: types.StateRunning,
}
path, err := createTempContainerIDMapping(testContainerID, testSandboxID)
assert.NoError(err)
defer os.RemoveAll(path)
testingImpl.StatusContainerFunc = func(ctx context.Context, sandboxID, containerID string) (vc.ContainerStatus, error) {
return newSingleContainerStatus(testContainerID, state, annotations, &ociSpec), nil
}
defer func() {
testingImpl.StatusContainerFunc = nil
}()
processJSON := `{
"consoleSize": {
"height": 15,
"width": 15
},
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM="
],
"cwd": "/"
}`
f, err := os.OpenFile(processPath, os.O_RDWR|os.O_CREATE, testFileMode)
assert.NoError(err)
_, err = f.WriteString(processJSON)
assert.NoError(err)
f.Close()
defer os.Remove(processPath)
workload := []string{"cat", "/dev/null"}
testingImpl.EnterContainerFunc = func(ctx context.Context, sandboxID, containerID string, cmd types.Cmd) (vc.VCSandbox, vc.VCContainer, *vc.Process, error) {
// create a fake container process
command := exec.Command(workload[0], workload[1:]...)
err := command.Start()
assert.NoError(err, "Unable to start process %v: %s", workload, err)
vcProcess := vc.Process{}
vcProcess.Pid = command.Process.Pid
return &vcmock.Sandbox{}, &vcmock.Container{}, &vcProcess, nil
}
defer func() {
testingImpl.EnterContainerFunc = nil
os.Remove(pidFilePath)
}()
fn, ok := execCLICommand.Action.(func(context *cli.Context) error)
assert.True(ok)
// vcAnnotations.EnvVars error due to incorrect environment
err = fn(ctx)
exitErr, ok := err.(*cli.ExitError)
assert.True(ok, true, "Exit code not received for fake workload process")
assert.Equal(exitErr.ExitCode(), 0, "Exit code should have been 0 for empty environment variable value")
}
func TestGenerateExecParams(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir("", "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
pidFilePath := filepath.Join(tmpdir, "pid")
consolePath := "/dev/ptmx"
consoleSocket := "/tmp/console-sock"
processLabel := "testlabel"
user := "root"
cwd := "cwd"
apparmor := "apparmorProf"
flagSet := flag.NewFlagSet("", 0)
flagSet.String("pid-file", pidFilePath, "")
flagSet.String("console", consolePath, "")
flagSet.String("console-socket", consoleSocket, "")
flagSet.String("process-label", processLabel, "")
flagSet.String("user", user, "")
flagSet.String("cwd", cwd, "")
flagSet.String("apparmor", apparmor, "")
ctx := createCLIContext(flagSet)
process := &specs.Process{}
params, err := generateExecParams(ctx, process)
assert.NoError(err)
assert.Equal(params.pidFile, pidFilePath)
assert.Equal(params.console, consolePath)
assert.Equal(params.consoleSock, consoleSocket)
assert.Equal(params.processLabel, processLabel)
assert.Equal(params.noSubreaper, false)
assert.Equal(params.detach, false)
assert.Equal(params.ociProcess.Terminal, false)
assert.Equal(params.ociProcess.User.UID, uint32(0))
assert.Equal(params.ociProcess.User.GID, uint32(0))
assert.Equal(params.ociProcess.Cwd, cwd)
assert.Equal(params.ociProcess.ApparmorProfile, apparmor)
}
func TestGenerateExecParamsWithProcessJsonFile(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir("", "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
pidFilePath := filepath.Join(tmpdir, "pid")
consolePath := "/dev/ptmx"
consoleSocket := "/tmp/console-sock"
detach := true
processLabel := "testlabel"
flagSet := flag.NewFlagSet("", 0)
flagSet.String("pid-file", pidFilePath, "")
flagSet.String("console", consolePath, "")
flagSet.String("console-socket", consoleSocket, "")
flagSet.Bool("detach", detach, "")
flagSet.String("process-label", processLabel, "")
processPath := filepath.Join(tmpdir, "process.json")
flagSet.String("process", processPath, "")
flagSet.Parse([]string{testContainerID})
ctx := createCLIContext(flagSet)
processJSON := `{
"consoleSize": {
"height": 15,
"width": 15
},
"terminal": true,
"user": {
"uid": 0,
"gid": 0
},
"args": [
"sh"
],
"env": [
"TERM=xterm",
"foo=bar"
],
"cwd": "/"
}`
f, err := os.OpenFile(processPath, os.O_RDWR|os.O_CREATE, testFileMode)
assert.NoError(err)
_, err = f.WriteString(processJSON)
assert.NoError(err)
f.Close()
defer os.Remove(processPath)
process := &specs.Process{}
params, err := generateExecParams(ctx, process)
assert.NoError(err)
assert.Equal(params.pidFile, pidFilePath)
assert.Equal(params.console, consolePath)
assert.Equal(params.consoleSock, consoleSocket)
assert.Equal(params.processLabel, processLabel)
assert.Equal(params.noSubreaper, false)
assert.Equal(params.detach, detach)
assert.Equal(params.ociProcess.Terminal, true)
assert.Equal(params.ociProcess.ConsoleSize.Height, uint(15))
assert.Equal(params.ociProcess.ConsoleSize.Width, uint(15))
assert.Equal(params.ociProcess.User.UID, uint32(0))
assert.Equal(params.ociProcess.User.GID, uint32(0))
assert.Equal(params.ociProcess.Cwd, "/")
assert.Equal(params.ociProcess.Env[0], "TERM=xterm")
assert.Equal(params.ociProcess.Env[1], "foo=bar")
}