Files
kata-containers/cli/list_test.go
Ace-Tang b7f51be8ce cli: do not fail on list when some containers bust
kata-runtime list command should list all valid container, not fail
when some containers information uncorrent, like rootfs not found.

Fixes: #1592

Signed-off-by: Ace-Tang <aceapril@126.com>
2019-04-29 17:04:15 +08:00

786 lines
18 KiB
Go

// Copyright (c) 2017 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
"github.com/kata-containers/runtime/pkg/katautils"
vc "github.com/kata-containers/runtime/virtcontainers"
vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations"
"github.com/kata-containers/runtime/virtcontainers/pkg/vcmock"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli"
)
type TestFileWriter struct {
Name string
File *os.File
}
var hypervisorDetails1 = hypervisorDetails{
HypervisorAsset: asset{
Path: "/hypervisor/path",
},
ImageAsset: asset{
Path: "/image/path",
},
KernelAsset: asset{
Path: "/kernel/path",
},
}
var hypervisorDetails2 = hypervisorDetails{
HypervisorAsset: asset{
Path: "/hypervisor/path2",
},
ImageAsset: asset{
Path: "/image/path2",
},
KernelAsset: asset{
Path: "/kernel/path2",
},
}
var hypervisorDetails3 = hypervisorDetails{
HypervisorAsset: asset{
Path: "/hypervisor/path3",
},
ImageAsset: asset{
Path: "/image/path3",
},
KernelAsset: asset{
Path: "/kernel/path3",
},
}
var testStatuses = []fullContainerState{
{
containerState: containerState{
Version: "",
ID: "1",
InitProcessPid: 1234,
Status: "running",
Bundle: "/somewhere/over/the/rainbow",
Created: time.Now().UTC(),
Annotations: map[string]string(nil),
Owner: "#0",
},
CurrentHypervisorDetails: hypervisorDetails1,
LatestHypervisorDetails: hypervisorDetails1,
StaleAssets: []string{},
},
{
containerState: containerState{
Version: "",
ID: "2",
InitProcessPid: 2345,
Status: "stopped",
Bundle: "/this/path/is/invalid",
Created: time.Now().UTC(),
Annotations: map[string]string(nil),
Owner: "#0",
},
CurrentHypervisorDetails: hypervisorDetails2,
LatestHypervisorDetails: hypervisorDetails2,
StaleAssets: []string{},
},
{
containerState: containerState{
Version: "",
ID: "3",
InitProcessPid: 9999,
Status: "ready",
Bundle: "/foo/bar/baz",
Created: time.Now().UTC(),
Annotations: map[string]string(nil),
Owner: "#0",
},
CurrentHypervisorDetails: hypervisorDetails3,
LatestHypervisorDetails: hypervisorDetails3,
StaleAssets: []string{},
},
}
// Implement the io.Writer interface
func (w *TestFileWriter) Write(bytes []byte) (n int, err error) {
return w.File.Write(bytes)
}
func formatListDataAsBytes(formatter formatState, state []fullContainerState, showAll bool) (bytes []byte, err error) {
tmpfile, err := ioutil.TempFile("", "formatListData-")
if err != nil {
return nil, err
}
defer os.Remove(tmpfile.Name())
err = formatter.Write(state, showAll, tmpfile)
if err != nil {
return nil, err
}
tmpfile.Close()
return ioutil.ReadFile(tmpfile.Name())
}
func formatListDataAsString(formatter formatState, state []fullContainerState, showAll bool) (lines []string, err error) {
bytes, err := formatListDataAsBytes(formatter, state, showAll)
if err != nil {
return nil, err
}
lines = strings.Split(string(bytes), "\n")
// Remove last line if empty
length := len(lines)
last := lines[length-1]
if last == "" {
lines = lines[:length-1]
}
return lines, nil
}
func TestStateToIDList(t *testing.T) {
// no header
expectedLength := len(testStatuses)
// showAll should not affect the output
for _, showAll := range []bool{true, false} {
lines, err := formatListDataAsString(&formatIDList{}, testStatuses, showAll)
if err != nil {
t.Fatal(err)
}
var expected []string
for _, s := range testStatuses {
expected = append(expected, s.ID)
}
length := len(lines)
if length != expectedLength {
t.Fatalf("Expected %d lines, got %d: %v", expectedLength, length, lines)
}
assert.Equal(t, lines, expected, "lines + expected")
}
}
func TestStateToTabular(t *testing.T) {
// +1 for header line
expectedLength := len(testStatuses) + 1
expectedDefaultHeaderPattern := `\AID\s+PID\s+STATUS\s+BUNDLE\s+CREATED\s+OWNER`
expectedExtendedHeaderPattern := `HYPERVISOR\s+KERNEL\s+IMAGE\s+LATEST-KERNEL\s+LATEST-IMAGE\s+STALE`
endingPattern := `\s*\z`
lines, err := formatListDataAsString(&formatTabular{}, testStatuses, false)
if err != nil {
t.Fatal(err)
}
length := len(lines)
expectedHeaderPattern := expectedDefaultHeaderPattern + endingPattern
expectedHeaderRE := regexp.MustCompile(expectedHeaderPattern)
if length != expectedLength {
t.Fatalf("Expected %d lines, got %d", expectedLength, length)
}
header := lines[0]
matches := expectedHeaderRE.FindAllStringSubmatch(header, -1)
if matches == nil {
t.Fatalf("Header line failed to match:\n"+
"pattern : %v\n"+
"line : %v\n",
expectedDefaultHeaderPattern,
header)
}
for i, status := range testStatuses {
lineIndex := i + 1
line := lines[lineIndex]
expectedLinePattern := fmt.Sprintf(`\A%s\s+%d\s+%s\s+%s\s+%s\s+%s\s*\z`,
regexp.QuoteMeta(status.ID),
status.InitProcessPid,
regexp.QuoteMeta(status.Status),
regexp.QuoteMeta(status.Bundle),
regexp.QuoteMeta(status.Created.Format(time.RFC3339Nano)),
regexp.QuoteMeta(status.Owner))
expectedLineRE := regexp.MustCompile(expectedLinePattern)
matches := expectedLineRE.FindAllStringSubmatch(line, -1)
if matches == nil {
t.Fatalf("Data line failed to match:\n"+
"pattern : %v\n"+
"line : %v\n",
expectedLinePattern,
line)
}
}
// Try again with full details this time
lines, err = formatListDataAsString(&formatTabular{}, testStatuses, true)
if err != nil {
t.Fatal(err)
}
length = len(lines)
expectedHeaderPattern = expectedDefaultHeaderPattern + `\s+` + expectedExtendedHeaderPattern + endingPattern
expectedHeaderRE = regexp.MustCompile(expectedHeaderPattern)
if length != expectedLength {
t.Fatalf("Expected %d lines, got %d", expectedLength, length)
}
header = lines[0]
matches = expectedHeaderRE.FindAllStringSubmatch(header, -1)
if matches == nil {
t.Fatalf("Header line failed to match:\n"+
"pattern : %v\n"+
"line : %v\n",
expectedDefaultHeaderPattern,
header)
}
for i, status := range testStatuses {
lineIndex := i + 1
line := lines[lineIndex]
expectedLinePattern := fmt.Sprintf(`\A%s\s+%d\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s+%s\s*\z`,
regexp.QuoteMeta(status.ID),
status.InitProcessPid,
regexp.QuoteMeta(status.Status),
regexp.QuoteMeta(status.Bundle),
regexp.QuoteMeta(status.Created.Format(time.RFC3339Nano)),
regexp.QuoteMeta(status.Owner),
regexp.QuoteMeta(status.CurrentHypervisorDetails.HypervisorAsset.Path),
regexp.QuoteMeta(status.CurrentHypervisorDetails.KernelAsset.Path),
regexp.QuoteMeta(status.CurrentHypervisorDetails.ImageAsset.Path),
regexp.QuoteMeta(status.LatestHypervisorDetails.KernelAsset.Path),
regexp.QuoteMeta(status.LatestHypervisorDetails.ImageAsset.Path),
regexp.QuoteMeta("-"))
expectedLineRE := regexp.MustCompile(expectedLinePattern)
matches := expectedLineRE.FindAllStringSubmatch(line, -1)
if matches == nil {
t.Fatalf("Data line failed to match:\n"+
"pattern : %v\n"+
"line : %v\n",
expectedLinePattern,
line)
}
}
}
func TestStateToJSON(t *testing.T) {
expectedLength := len(testStatuses)
// showAll should not affect the output
for _, showAll := range []bool{true, false} {
bytes, err := formatListDataAsBytes(&formatJSON{}, testStatuses, showAll)
if err != nil {
t.Fatal(err)
}
// Force capacity to match the original otherwise assert.Equal() complains.
states := make([]fullContainerState, 0, len(testStatuses))
err = json.Unmarshal(bytes, &states)
if err != nil {
t.Fatal(err)
}
length := len(states)
if length != expectedLength {
t.Fatalf("Expected %d lines, got %d", expectedLength, length)
}
// golang tip (what will presumably become v1.9) now
// stores a monotonic clock value as part of time.Time's
// internal representation (this is shown by a suffix in
// the form "m=±ddd.nnnnnnnnn" when calling String() on
// the time.Time object). However, this monotonic value
// is stripped out when marshaling.
//
// This behaviour change makes comparing the original
// object and the marshaled-and-then-unmarshaled copy of
// the object doomed to failure.
//
// The solution? Manually strip the monotonic time out
// of the original before comparison (yuck!)
//
// See:
//
// - https://go-review.googlesource.com/c/36255/7/src/time/time.go#54
//
for i := 0; i < expectedLength; i++ {
// remove monotonic time part
testStatuses[i].Created = testStatuses[i].Created.Truncate(0)
}
assert.Equal(t, states, testStatuses, "states + testStatuses")
}
}
func TestListCLIFunctionNoContainers(t *testing.T) {
ctx := createCLIContext(nil)
ctx.App.Name = "foo"
ctx.App.Metadata["foo"] = "bar"
fn, ok := listCLICommand.Action.(func(context *cli.Context) error)
assert.True(t, ok)
err := fn(ctx)
// no config in the Metadata
assert.Error(t, err)
}
func TestListGetContainersListSandboxFail(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir(testDir, "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
ctx := createCLIContext(nil)
ctx.App.Name = "foo"
runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
assert.NoError(err)
ctx.App.Metadata = map[string]interface{}{
"runtimeConfig": runtimeConfig,
}
_, err = getContainers(context.Background(), ctx)
assert.Error(err)
assert.True(vcmock.IsMockError(err))
}
func TestListGetContainers(t *testing.T) {
assert := assert.New(t)
testingImpl.ListSandboxFunc = func(ctx context.Context) ([]vc.SandboxStatus, error) {
// No pre-existing sandboxes
return []vc.SandboxStatus{}, nil
}
defer func() {
testingImpl.ListSandboxFunc = nil
}()
tmpdir, err := ioutil.TempDir(testDir, "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
ctx := createCLIContext(nil)
ctx.App.Name = "foo"
runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
assert.NoError(err)
ctx.App.Metadata = map[string]interface{}{
"runtimeConfig": runtimeConfig,
}
state, err := getContainers(context.Background(), ctx)
assert.NoError(err)
assert.Equal(state, []fullContainerState(nil))
}
func TestListGetContainersSandboxWithoutContainers(t *testing.T) {
assert := assert.New(t)
sandbox := &vcmock.Sandbox{
MockID: testSandboxID,
}
testingImpl.ListSandboxFunc = func(ctx context.Context) ([]vc.SandboxStatus, error) {
return []vc.SandboxStatus{
{
ID: sandbox.ID(),
ContainersStatus: []vc.ContainerStatus(nil),
},
}, nil
}
defer func() {
testingImpl.ListSandboxFunc = nil
}()
tmpdir, err := ioutil.TempDir(testDir, "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
ctx := createCLIContext(nil)
ctx.App.Name = "foo"
runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
assert.NoError(err)
ctx.App.Metadata = map[string]interface{}{
"runtimeConfig": runtimeConfig,
}
state, err := getContainers(context.Background(), ctx)
assert.NoError(err)
assert.Equal(state, []fullContainerState(nil))
}
func TestListGetContainersSandboxWithContainer(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir(testDir, "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
sandbox := &vcmock.Sandbox{
MockID: testSandboxID,
}
rootfs := filepath.Join(tmpdir, "rootfs")
err = os.MkdirAll(rootfs, testDirMode)
assert.NoError(err)
testingImpl.ListSandboxFunc = func(ctx context.Context) ([]vc.SandboxStatus, error) {
return []vc.SandboxStatus{
{
ID: sandbox.ID(),
ContainersStatus: []vc.ContainerStatus{
{
ID: sandbox.ID(),
Annotations: map[string]string{},
RootFs: rootfs,
},
},
},
}, nil
}
defer func() {
testingImpl.ListSandboxFunc = nil
}()
ctx := createCLIContext(nil)
ctx.App.Name = "foo"
runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
assert.NoError(err)
ctx.App.Metadata["runtimeConfig"] = runtimeConfig
_, err = getContainers(context.Background(), ctx)
assert.NoError(err)
}
func TestListCLIFunctionFormatFail(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir(testDir, "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
quietFlags := flag.NewFlagSet("test", 0)
quietFlags.Bool("quiet", true, "")
tableFlags := flag.NewFlagSet("test", 0)
tableFlags.String("format", "table", "")
jsonFlags := flag.NewFlagSet("test", 0)
jsonFlags.String("format", "json", "")
invalidFlags := flag.NewFlagSet("test", 0)
invalidFlags.String("format", "not-a-valid-format", "")
type testData struct {
format string
flags *flag.FlagSet
}
data := []testData{
{"quiet", quietFlags},
{"table", tableFlags},
{"json", jsonFlags},
{"invalid", invalidFlags},
}
sandbox := &vcmock.Sandbox{
MockID: testSandboxID,
}
rootfs := filepath.Join(tmpdir, "rootfs")
testingImpl.ListSandboxFunc = func(ctx context.Context) ([]vc.SandboxStatus, error) {
return []vc.SandboxStatus{
{
ID: sandbox.ID(),
ContainersStatus: []vc.ContainerStatus{
{
ID: sandbox.ID(),
Annotations: map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
},
RootFs: rootfs,
},
},
},
}, nil
}
defer func() {
testingImpl.ListSandboxFunc = nil
}()
savedOutputFile := defaultOutputFile
defer func() {
defaultOutputFile = savedOutputFile
}()
// purposely invalid
var invalidFile *os.File
for _, d := range data {
// start off with an invalid output file
defaultOutputFile = invalidFile
ctx := createCLIContext(d.flags)
ctx.App.Name = "foo"
ctx.App.Metadata["foo"] = "bar"
fn, ok := listCLICommand.Action.(func(context *cli.Context) error)
assert.True(ok, d)
err = fn(ctx)
// no config in the Metadata
assert.Error(err, d)
runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
assert.NoError(err, d)
ctx.App.Metadata["runtimeConfig"] = runtimeConfig
err = os.MkdirAll(rootfs, testDirMode)
assert.NoError(err)
err = fn(ctx)
// invalid output file
assert.Error(err, d)
assert.False(vcmock.IsMockError(err), d)
output := filepath.Join(tmpdir, "output")
f, err := os.OpenFile(output, os.O_WRONLY|os.O_CREATE, testFileMode)
assert.NoError(err)
defer f.Close()
// output file is now valid
defaultOutputFile = f
err = fn(ctx)
if d.format == "invalid" {
assert.Error(err)
assert.False(vcmock.IsMockError(err), d)
} else {
assert.NoError(err)
}
}
}
func TestListCLIFunctionQuiet(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir(testDir, "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
assert.NoError(err)
sandbox := &vcmock.Sandbox{
MockID: testSandboxID,
}
rootfs := filepath.Join(tmpdir, "rootfs")
err = os.MkdirAll(rootfs, testDirMode)
assert.NoError(err)
testingImpl.ListSandboxFunc = func(ctx context.Context) ([]vc.SandboxStatus, error) {
return []vc.SandboxStatus{
{
ID: sandbox.ID(),
ContainersStatus: []vc.ContainerStatus{
{
ID: sandbox.ID(),
Annotations: map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
},
RootFs: rootfs,
},
},
},
}, nil
}
defer func() {
testingImpl.ListSandboxFunc = nil
}()
set := flag.NewFlagSet("test", 0)
set.Bool("quiet", true, "")
ctx := createCLIContext(set)
ctx.App.Name = "foo"
ctx.App.Metadata["runtimeConfig"] = runtimeConfig
savedOutputFile := defaultOutputFile
defer func() {
defaultOutputFile = savedOutputFile
}()
output := filepath.Join(tmpdir, "output")
f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_SYNC, testFileMode)
assert.NoError(err)
defer f.Close()
defaultOutputFile = f
fn, ok := listCLICommand.Action.(func(context *cli.Context) error)
assert.True(ok)
err = fn(ctx)
assert.NoError(err)
f.Close()
text, err := katautils.GetFileContents(output)
assert.NoError(err)
trimmed := strings.TrimSpace(text)
assert.Equal(testSandboxID, trimmed)
}
func TestListGetDirOwner(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir(testDir, "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
_, err = getDirOwner("")
// invalid parameter
assert.Error(err)
dir := filepath.Join(tmpdir, "dir")
_, err = getDirOwner(dir)
// ENOENT
assert.Error(err)
err = createEmptyFile(dir)
assert.NoError(err)
_, err = getDirOwner(dir)
// wrong file type
assert.Error(err)
err = os.Remove(dir)
assert.NoError(err)
err = os.MkdirAll(dir, testDirMode)
assert.NoError(err)
uid := uint32(os.Getuid())
dirUID, err := getDirOwner(dir)
assert.NoError(err)
assert.Equal(dirUID, uid)
}
func TestListWithRootfsMissShouldSuccess(t *testing.T) {
assert := assert.New(t)
tmpdir, err := ioutil.TempDir(testDir, "")
assert.NoError(err)
defer os.RemoveAll(tmpdir)
sandbox := &vcmock.Sandbox{
MockID: testSandboxID,
}
rootfs := filepath.Join(tmpdir, "rootfs")
err = os.MkdirAll(rootfs, testDirMode)
assert.NoError(err)
testingImpl.ListSandboxFunc = func(ctx context.Context) ([]vc.SandboxStatus, error) {
return []vc.SandboxStatus{
{
ID: sandbox.ID(),
ContainersStatus: []vc.ContainerStatus{
{
ID: sandbox.ID(),
Annotations: map[string]string{
vcAnnotations.ContainerTypeKey: string(vc.PodSandbox),
},
RootFs: rootfs,
},
},
},
}, nil
}
defer func() {
testingImpl.ListSandboxFunc = nil
}()
set := flag.NewFlagSet("test", 0)
set.String("format", "table", "")
ctx := createCLIContext(set)
ctx.App.Name = "foo"
runtimeConfig, err := newTestRuntimeConfig(tmpdir, testConsole, true)
assert.NoError(err)
ctx.App.Metadata["runtimeConfig"] = runtimeConfig
fn, ok := listCLICommand.Action.(func(context *cli.Context) error)
assert.True(ok)
err = fn(ctx)
assert.NoError(err)
// remove container rootfs, check list command should also work
assert.NoError(os.RemoveAll(rootfs))
err = fn(ctx)
assert.NoError(err)
}