diff --git a/containerm/cli/cli_command_ctrs_list.go b/containerm/cli/cli_command_ctrs_list.go index 878f490..e25e203 100644 --- a/containerm/cli/cli_command_ctrs_list.go +++ b/containerm/cli/cli_command_ctrs_list.go @@ -17,6 +17,7 @@ import ( "fmt" "os" "strconv" + "strings" "text/tabwriter" "github.com/eclipse-kanto/container-management/containerm/client" @@ -30,7 +31,9 @@ type listCmd struct { } type listConfig struct { - name string + name string + quiet bool + filter []string } func (cc *listCmd) init(cli *cli) { @@ -43,7 +46,7 @@ func (cc *listCmd) init(cli *cli) { RunE: func(cmd *cobra.Command, args []string) error { return cc.run(args) }, - Example: "list\n list --name \n list -n ", + Example: " list\n list --name \n list --quiet\n list --filter status=created", } cc.setupFlags() } @@ -57,6 +60,23 @@ func (cc *listCmd) run(args []string) error { if err != nil { return err } + if len(cc.config.filter) > 0 { + filtered, err := filterBy(cc.config.filter, ctrs) + if err != nil { + return err + } + ctrs = filtered + } + if cc.config.quiet { + for i, ctr := range ctrs { + if i != len(ctrs)-1 { + fmt.Printf("%s ", ctr.ID) + } else { + fmt.Println(ctr.ID) + } + } + return nil + } if len(ctrs) == 0 { fmt.Println("No found containers.") } else { @@ -69,6 +89,45 @@ func (cc *listCmd) setupFlags() { flagSet := cc.cmd.Flags() // init name flags flagSet.StringVarP(&cc.config.name, "name", "n", "", "List all containers with a specific name.") + flagSet.BoolVarP(&cc.config.quiet, "quiet", "q", false, "List only container IDs.") + flagSet.StringSliceVar(&cc.config.filter, "filter", nil, "Lists only containers with a specified filter. The containers can be filtered by their status, image and exitcode.") +} + +func filterBy(input []string, ctrs []*types.Container) ([]*types.Container, error) { + var ( + holderStatus string + holderImage string + convertedExitCode int = -1 + err error + ) + filteredCtrs := []*types.Container{} + for _, inp := range input { + if strings.HasPrefix(inp, "status=") { + holderStatus = strings.TrimPrefix(inp, "status=") + } else if strings.HasPrefix(inp, "image=") { + holderImage = strings.TrimPrefix(inp, "image=") + } else if strings.HasPrefix(inp, "exitcode=") { + convertedExitCode, err = strconv.Atoi(strings.TrimPrefix(inp, "exitcode=")) + if err != nil { + return nil, err + } + } else { + return nil, fmt.Errorf("no filter: %s", strings.Split(inp, "=")[0]) + } + } + for _, ctr := range ctrs { + if holderStatus != "" && !strings.EqualFold(ctr.State.Status.String(), holderStatus) { + continue + } + if holderImage != "" && !strings.EqualFold(ctr.Image.Name, holderImage) { + continue + } + if int64(convertedExitCode) != -1 && ctr.State.ExitCode != int64(convertedExitCode) { + continue + } + filteredCtrs = append(filteredCtrs, ctr) + } + return filteredCtrs, nil } /* diff --git a/containerm/cli/cli_command_ctrs_list_test.go b/containerm/cli/cli_command_ctrs_list_test.go index 883162a..ee95de3 100644 --- a/containerm/cli/cli_command_ctrs_list_test.go +++ b/containerm/cli/cli_command_ctrs_list_test.go @@ -14,6 +14,7 @@ package main import ( "context" + "fmt" "testing" "github.com/eclipse-kanto/container-management/containerm/client" @@ -24,7 +25,9 @@ import ( const ( // command flags - listCmdFlagName = "name" + listCmdFlagName = "name" + listCmdFlagQuiet = "quiet" + listCmdFlagFilter = "filter" // test input constants listContainerID = "test-ctr" @@ -117,6 +120,42 @@ func (listTc *listCommandTest) generateRunExecutionConfigs() map[string]testRunE }, mockExecution: listTc.mockExecListByNameNoCtrs, }, + "test_list_quiet": { + flags: map[string]string{ + listCmdFlagQuiet: "true", + }, + mockExecution: listTc.mockExecListQuiet, + }, + "test_list_with_filter_status": { + flags: map[string]string{ + listCmdFlagFilter: "status=creating", + }, + mockExecution: listTc.mockExecListWithFilter, + }, + "test_list_with_filter_image": { + flags: map[string]string{ + listCmdFlagFilter: "image=test", + }, + mockExecution: listTc.mockExecListWithFilter, + }, + "test_list_with_filter_exit_code": { + flags: map[string]string{ + listCmdFlagFilter: "exitcode=0", + }, + mockExecution: listTc.mockExecListWithFilter, + }, + "test_list_with_multiple_filters": { + flags: map[string]string{ + listCmdFlagFilter: "image=test,exitcode=0", + }, + mockExecution: listTc.mockExecListWithFilter, + }, + "test_list_with_filter_error": { + flags: map[string]string{ + listCmdFlagFilter: "test=test", + }, + mockExecution: listTc.mockExecListWithFilterError, + }, "test_list_by_name_err": { flags: map[string]string{ listCmdFlagName: listFlagName, @@ -172,6 +211,48 @@ func (listTc *listCommandTest) mockExecListByNameNoCtrs(args []string) error { return nil } +func (listTc *listCommandTest) mockExecListQuiet(args []string) error { + // setup expected calls + ctrs := []*types.Container{ + { + ID: fmt.Sprintf("%s-%d", listContainerID, 1), + Name: listFlagName, + State: &types.State{}, + }, + { + ID: fmt.Sprintf("%s-%d", listContainerID, 2), + Name: listFlagName, + State: &types.State{}, + }, + } + listTc.mockClient.EXPECT().List(context.Background()).Times(1).Return(ctrs, nil) + // no error expected + return nil +} + +func (listTc *listCommandTest) mockExecListWithFilter(args []string) error { + // setup expected calls + ctrs := []*types.Container{{ + ID: listContainerID, + Name: listFlagName, + State: &types.State{}, + }} + listTc.mockClient.EXPECT().List(context.Background()).Times(1).Return(ctrs, nil) + // no error expected + return nil +} + +func (listTc *listCommandTest) mockExecListWithFilterError(args []string) error { + err := log.NewError("no filter: test") + ctrs := []*types.Container{{ + ID: listContainerID, + Name: listFlagName, + State: &types.State{}, + }} + listTc.mockClient.EXPECT().List(context.Background()).Times(1).Return(ctrs, nil) + return err +} + func (listTc *listCommandTest) mockExecListByNameErrors(args []string) error { // setup expected calls err := log.NewError("failed to get containers list") diff --git a/containerm/cli/cli_command_ctrs_stop.go b/containerm/cli/cli_command_ctrs_stop.go index 3a4336f..7ca2a02 100644 --- a/containerm/cli/cli_command_ctrs_stop.go +++ b/containerm/cli/cli_command_ctrs_stop.go @@ -44,7 +44,7 @@ func (cc *stopCmd) init(cli *cli) { RunE: func(cmd *cobra.Command, args []string) error { return cc.run(args) }, - Example: "stop \n stop --name \n stop -n ", + Example: " stop \n stop --name \n stop -n ", } cc.setupFlags() } @@ -75,7 +75,7 @@ func (cc *stopCmd) run(args []string) error { func (cc *stopCmd) setupFlags() { flagSet := cc.cmd.Flags() // init timeout flag - flagSet.Int64VarP(&cc.config.timeout, "timeout", "t", math.MinInt64, "Sets the timeout period in seconds to gracefully stop the container. When timeout expires the container process would be forcibly killed.") + flagSet.Int64VarP(&cc.config.timeout, "time", "t", math.MinInt64, "Sets the timeout period in seconds to gracefully stop the container. When timeout expires the container process would be forcibly killed.") // init name flag flagSet.StringVarP(&cc.config.name, "name", "n", "", "Stop a container with a specific name.") // init force flag diff --git a/containerm/cli/cli_command_ctrs_stop_test.go b/containerm/cli/cli_command_ctrs_stop_test.go index 9c17e95..3f3c29e 100644 --- a/containerm/cli/cli_command_ctrs_stop_test.go +++ b/containerm/cli/cli_command_ctrs_stop_test.go @@ -26,7 +26,7 @@ import ( const ( // command flags - stopCmdFlagTimeout = "timeout" + stopCmdFlagTimeout = "time" stopCmdFlagName = "name" stopCmdFlagForce = "force" stopCmdFlagSignal = "signal" diff --git a/integration/framework/cli/cmd_base.go b/integration/framework/cli/cmd_base.go index 4e27b52..ed103ad 100644 --- a/integration/framework/cli/cmd_base.go +++ b/integration/framework/cli/cmd_base.go @@ -105,7 +105,8 @@ func RunCmdTestCases(t *testing.T, cmdList []TestCaseCMD) { if cmd.setupCmd != nil { runMultipleCommands(t, *cmd.setupCmd) } - result := icmd.RunCmd(cmd.icmd) + checkArguments(t, &cmd.icmd) + result := icmd.RunCommand(cmd.icmd.Command[0], cmd.icmd.Command[1:]...) if cmd.goldenFile != "" { assert.Assert(t, golden.String(result.Stdout(), cmd.goldenFile)) } @@ -122,25 +123,36 @@ func RunCmdTestCases(t *testing.T, cmdList []TestCaseCMD) { } } -func runMultipleCommands(t *testing.T, cmdArr []icmd.Cmd) { - for _, cmd := range cmdArr { - result := icmd.RunCmd(cmd) - result.Assert(t, icmd.Expected{ExitCode: 0}) - } -} - -func buildCmd(binary string, args ...string) *icmd.Cmd { - envArgs := []string{} - for _, arg := range args { +func checkArguments(t *testing.T, cmd *icmd.Cmd) { + execCmd := []string{} + for _, arg := range cmd.Command { + if strings.HasPrefix(arg, "$(") && strings.HasSuffix(arg, ")") { + arg = strings.TrimPrefix(arg, "$(") + arg = strings.TrimSuffix(arg, ")") + arguments := strings.Split(arg, " ") + cmd := icmd.Command(arguments[0], arguments[1:]...) + checkArguments(t, &cmd) + result := icmd.RunCmd(cmd) + assert.Equal(t, result.ExitCode, 0) + execCmd = append(execCmd, strings.Split(strings.TrimSuffix(strings.TrimSuffix(result.Stdout(), "\n"), " "), " ")...) + continue + } if strings.HasPrefix(arg, "$") { if val, ok := os.LookupEnv(strings.TrimPrefix(arg, "$")); ok { arg = val } } - envArgs = append(envArgs, arg) + execCmd = append(execCmd, arg) + } + *cmd = icmd.Cmd{Command: execCmd} +} + +func runMultipleCommands(t *testing.T, cmdArr []icmd.Cmd) { + for _, cmd := range cmdArr { + checkArguments(t, &cmd) + result := icmd.RunCommand(cmd.Command[0], cmd.Command[1:]...) + result.Assert(t, icmd.Expected{ExitCode: 0}) } - cmd := icmd.Command(binary, envArgs...) - return &cmd } func assertCustomResult(t *testing.T, result icmd.Result, name string, args ...string) { @@ -152,7 +164,7 @@ func assertCustomResult(t *testing.T, result icmd.Result, name string, args ...s func fromAPITestCommand(cmd TestCommand) TestCaseCMD { return TestCaseCMD{ name: cmd.Name, - icmd: *buildCmd(cmd.Command.Binary, cmd.Command.Args...), + icmd: icmd.Command(cmd.Command.Binary, cmd.Command.Args...), expected: icmd.Expected{ ExitCode: cmd.Expected.ExitCode, Timeout: cmd.Expected.Timeout, @@ -174,7 +186,7 @@ func buildCmdArrFromCommand(cmd *[]Command) *[]icmd.Cmd { } cmds := make([]icmd.Cmd, 0) for _, cmd := range *cmd { - cmds = append(cmds, *buildCmd(cmd.Binary, cmd.Args...)) + cmds = append(cmds, icmd.Command(cmd.Binary, cmd.Args...)) } return &cmds } diff --git a/integration/testdata/list-help.golden b/integration/testdata/list-help.golden index 32f3de7..c05fbf1 100644 --- a/integration/testdata/list-help.golden +++ b/integration/testdata/list-help.golden @@ -4,13 +4,16 @@ Usage: kanto-cm list Examples: -list + list list --name - list -n + list --quiet + list --filter status=created Flags: - -h, --help help for list - -n, --name string List all containers with a specific name. + --filter strings Lists only containers with a specified filter. The containers can be filtered by their status, image and exitcode. + -h, --help help for list + -n, --name string List all containers with a specific name. + -q, --quiet List only container IDs. Global Flags: --debug Switch commands log level to DEBUG mode diff --git a/integration/testdata/list-test.yaml b/integration/testdata/list-test.yaml index fc3bf9f..a7e685f 100644 --- a/integration/testdata/list-test.yaml +++ b/integration/testdata/list-test.yaml @@ -44,11 +44,46 @@ onExit: - binary: "kanto-cm" args: ["stop", "--host", "$KANTO_HOST", "-s", "SIGKILL", "-n", "list_containers_with_state_running"] - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "list_containers_with_state_running", "-f"] + args: ["remove", "--host", "$KANTO_HOST", "$(kanto-cm list --quiet)"] +--- +name: list_containers_with_quiet +setupCmd: + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "list_containers_with_quiet_one", "docker.io/library/influxdb:1.8.4"] + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "list_containers_with_quiet_two", "docker.io/library/influxdb:1.8.4"] +command: + binary: kanto-cm + args: ["list", "--host", "$KANTO_HOST", "--quiet"] +expected: + exitCode: 0 +customResult: + type: REGEX + args: ["[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"] +onExit: - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "list_containers_with_state_stopped", "-f"] + args: ["remove", "--host", "$KANTO_HOST", "$(kanto-cm list --quiet)"] +--- +name: list_containers_with_filter +setupCmd: + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "list_containers_with_filter_one", "docker.io/library/influxdb:1.8.4"] + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "list_containers_with_filter_two", "docker.io/library/influxdb:1.8.4"] +command: + binary: kanto-cm + args: ["list", "--host", "$KANTO_HOST", "--filter", "status=created"] +expected: + exitCode: 0 +customResult: + type: REGEX + args: ["ID |Name |Image |Status |Finished At |Exit Code | + ------------------------------------- |------------------------------------- |------------------------------------------------------------ |---------- |------------------------------ |---------- | + [0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12} |list_containers_with_filter_one |docker.io/library/influxdb:1.8.4 |Created | |0 | + [0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12} |list_containers_with_filter_two |docker.io/library/influxdb:1.8.4 |Created | |0 |"] +onExit: - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "list_containers_with_state_created", "-f"] + args: ["remove", "--host", "$KANTO_HOST", "$(kanto-cm list --quiet)"] --- name: list_existing_container setupCmd: @@ -69,9 +104,7 @@ customResult: ] onExit: - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "list_containers_ctr0", "-f"] - - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "list_containers_ctr1", "-f"] + args: ["remove", "--host", "$KANTO_HOST", "$(kanto-cm list --quiet)"] --- name: list_not_existing_container command: diff --git a/integration/testdata/remove-test.yaml b/integration/testdata/remove-test.yaml index dfec002..3de6ac9 100644 --- a/integration/testdata/remove-test.yaml +++ b/integration/testdata/remove-test.yaml @@ -58,4 +58,16 @@ onExit: - binary: kanto-cm args: ["stop", "--host", "$KANTO_HOST", "-s", "SIGKILL", "-n", "remove_container_with_state_running"] - binary: "kanto-cm" - args: ["remove", "--host", "$KANTO_HOST", "-n", "remove_container_with_state_running", "-f"] \ No newline at end of file + args: ["remove", "--host", "$KANTO_HOST", "-n", "remove_container_with_state_running", "-f"] +--- +name: remove_multiple_containers +setupCmd: + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "remove_container_one", "docker.io/library/influxdb:1.8.4"] + - binary: kanto-cm + args: ["create", "--host", "$KANTO_HOST", "-n", "remove_container_two", "docker.io/library/influxdb:1.8.4"] +command: + binary: kanto-cm + args: ["remove", "--host", "$KANTO_HOST", "$(kanto-cm list --quiet)"] +expected: + exitCode: 0 \ No newline at end of file diff --git a/integration/testdata/stop-help.golden b/integration/testdata/stop-help.golden index c423665..48f0ec9 100644 --- a/integration/testdata/stop-help.golden +++ b/integration/testdata/stop-help.golden @@ -4,7 +4,7 @@ Usage: kanto-cm stop Examples: -stop + stop stop --name stop -n @@ -13,6 +13,7 @@ Flags: -h, --help help for stop -n, --name string Stop a container with a specific name. -s, --signal string Stop a container using a specific signal. Signals could be specified by using their names or numbers, e.g. SIGINT or 2. (default "SIGTERM") + -t, --time int Sets the timeout period in seconds to gracefully stop the container. When timeout expires the container process would be forcibly killed. (default -9223372036854775808) Global Flags: --debug Switch commands log level to DEBUG mode diff --git a/integration/testdata/stop-test.yaml b/integration/testdata/stop-test.yaml index 1d7ac45..dac4350 100644 --- a/integration/testdata/stop-test.yaml +++ b/integration/testdata/stop-test.yaml @@ -1,4 +1,4 @@ -name: remove_help +name: stop_help command: binary: kanto-cm args: ["stop", "-h"]