diff --git a/go.mod b/go.mod index 384842555..a1df5aec6 100644 --- a/go.mod +++ b/go.mod @@ -14,9 +14,9 @@ require ( github.com/pmorie/go-open-service-broker-client v0.0.0-20180330214919-dca737037ce6 github.com/sabhiram/go-gitignore v0.0.0-20171017070213-362f9845770f github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa - github.com/tsuru/go-tsuruclient v0.0.0-20231005173907-4607cc1f111e + github.com/tsuru/go-tsuruclient v0.0.0-20231009130311-a01dfd615e16 github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6 - github.com/tsuru/tsuru v0.0.0-20231003184130-e29d84f36397 + github.com/tsuru/tsuru v0.0.0-20231009130140-65592312e508 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v2 v2.4.0 k8s.io/apimachinery v0.23.17 diff --git a/go.sum b/go.sum index b768c3622..a37e8a33a 100644 --- a/go.sum +++ b/go.sum @@ -733,14 +733,12 @@ github.com/tsuru/config v0.0.0-20201023175036-375aaee8b560 h1:fniQ/BmYAHdnNmY333 github.com/tsuru/config v0.0.0-20201023175036-375aaee8b560/go.mod h1:mj6t8JKWU51GScTT50XRmDj65T5XhTyNvO5FUNV5zS4= github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa h1:JlLQP1xa13a994p/Aau2e3K9xXYaHNoNvTDVIMHSUa4= github.com/tsuru/gnuflag v0.0.0-20151217162021-86b8c1b864aa/go.mod h1:UibOSvkMFKRe/eiwktAPAvQG8L+p8nYsECJvu3Dgw7I= -github.com/tsuru/go-tsuruclient v0.0.0-20231004185254-b386081b2ca8 h1:ne140iJLe0drfwdz4cvTrTf/rmyirD8VI+Ivm+l6XxU= -github.com/tsuru/go-tsuruclient v0.0.0-20231004185254-b386081b2ca8/go.mod h1:BmePxHey9hxrxk0kzTMHFFr7aJWXSxtlrUx6FIeV0Ic= -github.com/tsuru/go-tsuruclient v0.0.0-20231005173907-4607cc1f111e h1:SyzgPCFdzo4u8BKIXKGm5+wqSSX5kttnsjMPEzThl3I= -github.com/tsuru/go-tsuruclient v0.0.0-20231005173907-4607cc1f111e/go.mod h1:BmePxHey9hxrxk0kzTMHFFr7aJWXSxtlrUx6FIeV0Ic= +github.com/tsuru/go-tsuruclient v0.0.0-20231009130311-a01dfd615e16 h1:gjwhjJTOuPlHhytkBXvfEzIzyYytePVvGeq7REbeBGY= +github.com/tsuru/go-tsuruclient v0.0.0-20231009130311-a01dfd615e16/go.mod h1:BmePxHey9hxrxk0kzTMHFFr7aJWXSxtlrUx6FIeV0Ic= github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6 h1:1XDdWFAjIbCSG1OjN9v9KdWhuM8UtYlFcfHe/Ldkchk= github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6/go.mod h1:ztYpOhW+u1k21FEqp7nZNgpWbr0dUKok5lgGCZi+1AQ= -github.com/tsuru/tsuru v0.0.0-20231003184130-e29d84f36397 h1:9NpNEFIUgmEPI4H+ngFQGisRrpHv2UWKoiPApXUaCfI= -github.com/tsuru/tsuru v0.0.0-20231003184130-e29d84f36397/go.mod h1:is8CUBIZaH1mwyvvL3br5Bts/a29iTAQqEQbV7jSOJQ= +github.com/tsuru/tsuru v0.0.0-20231009130140-65592312e508 h1:eaMg/uBeTv6B7O+AMTq3OKqNsiA0/kB5Zkgy7ipXgcI= +github.com/tsuru/tsuru v0.0.0-20231009130140-65592312e508/go.mod h1:is8CUBIZaH1mwyvvL3br5Bts/a29iTAQqEQbV7jSOJQ= github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= diff --git a/tsuru/client/apps.go b/tsuru/client/apps.go index 6cc54518e..626cd61f8 100644 --- a/tsuru/client/apps.go +++ b/tsuru/client/apps.go @@ -219,13 +219,13 @@ type AppUpdate struct { cmd.AppNameMixIn cmd.ConfirmationCommand - memory, cpu string + memory, cpu, cpuBurst string } func (c *AppUpdate) Info() *cmd.Info { return &cmd.Info{ Name: "app-update", - Usage: "app update [-a/--app appname] [--description/-d description] [--plan/-p plan name] [--pool/-o pool] [--team-owner/-t team owner] [--platform/-l platform] [-i/--image-reset] [--cpu cpu] [--memory memory] [--tag/-g tag]...", + Usage: "app update [-a/--app appname] [--description/-d description] [--plan/-p plan name] [--pool/-o pool] [--team-owner/-t team owner] [--platform/-l platform] [-i/--image-reset] [--cpu cpu] [--memory memory] [--cpu-burst-factor cpu-burst-factor] [--tag/-g tag]...", Desc: `Updates an app, changing its description, tags, plan or pool information.`, } } @@ -257,6 +257,8 @@ func (c *AppUpdate) Flags() *gnuflag.FlagSet { flagSet.Var((*cmd.StringSliceFlag)(&c.args.Tags), "g", tagMessage) flagSet.Var((*cmd.StringSliceFlag)(&c.args.Tags), "tag", tagMessage) flagSet.StringVar(&c.cpu, "cpu", "", "CPU limit for app, this will override the plan cpu value. One cpu is equivalent to 1 vCPU/Core, fractional requests are allowed and the expression 0.1 is equivalent to the expression 100m") + flagSet.StringVar(&c.cpuBurst, "cpu-burst-factor", "", "The multiplier to determine the limits of the CPU burst. Setting 1 disables burst") + flagSet.StringVar(&c.memory, "memory", "", "Memory limit for app, this will override the plan memory value. You can express memory as a bytes integer or using one of these suffixes: E, P, T, G, M, K, Ei, Pi, Ti, Gi, Mi, Ki") c.fs = cmd.MergeFlagSet( c.AppNameMixIn.Flags(), @@ -296,6 +298,20 @@ func (c *AppUpdate) Run(ctx *cmd.Context, cli *cmd.Client) error { c.args.Planoverride.Memory = &val } + if c.cpuBurst != "" { + var cpuBurst float64 + cpuBurst, err = strconv.ParseFloat(c.cpuBurst, 64) + if err != nil { + return err + } + + if cpuBurst < 1 { + return errors.New("Invalid factor, please use a value greater equal 1") + } + + c.args.Planoverride.CpuBurst = &cpuBurst + } + appName := c.Flags().Lookup("app").Value.String() if appName == "" { return errors.New("Please use the -a/--app flag to specify which app you want to update.") @@ -748,7 +764,7 @@ func (a *app) String(simplified bool) string { if !simplified && (a.Plan.Memory != 0 || a.Plan.CPUMilli != 0) { buf.WriteString("\n") buf.WriteString("App Plan:\n") - buf.WriteString(renderPlans([]apptypes.Plan{a.Plan}, false, false)) + buf.WriteString(renderPlans([]apptypes.Plan{a.Plan}, renderPlansOpts{})) } if !simplified && internalAddressesTable.Rows() > 0 { buf.WriteString("\n") diff --git a/tsuru/client/apps_test.go b/tsuru/client/apps_test.go index 8951b64bd..97d061b89 100644 --- a/tsuru/client/apps_test.go +++ b/tsuru/client/apps_test.go @@ -613,6 +613,73 @@ func (s *S) TestAppUpdateWithCPUAndMemory(c *check.C) { c.Assert(stdout.String(), check.Equals, expected) } +func (s *S) TestAppUpdateWithCPUBurst(c *check.C) { + var stdout, stderr bytes.Buffer + expected := fmt.Sprintf("App %q has been updated!\n", "ble") + context := cmd.Context{ + Stdout: &stdout, + Stderr: &stderr, + } + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + url := strings.HasSuffix(req.URL.Path, "/apps/ble") + method := req.Method == "PUT" + data, err := io.ReadAll(req.Body) + c.Assert(err, check.IsNil) + var result map[string]interface{} + err = json.Unmarshal(data, &result) + c.Assert(err, check.IsNil) + c.Assert(result, check.DeepEquals, map[string]interface{}{ + "planoverride": map[string]interface{}{ + "cpuBurst": float64(1.3), + }, + "metadata": map[string]interface{}{}, + }) + return url && method + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + command := AppUpdate{} + command.Flags().Parse(true, []string{"-a", "ble", "--cpu-burst-factor", "1.3"}) + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(stdout.String(), check.Equals, expected) +} + +func (s *S) TestAppUpdateWithInvalidCPUBurst(c *check.C) { + var stdout, stderr bytes.Buffer + context := cmd.Context{ + Stdout: &stdout, + Stderr: &stderr, + } + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + url := strings.HasSuffix(req.URL.Path, "/apps/ble") + method := req.Method == "PUT" + data, err := io.ReadAll(req.Body) + c.Assert(err, check.IsNil) + var result map[string]interface{} + err = json.Unmarshal(data, &result) + c.Assert(err, check.IsNil) + c.Assert(result, check.DeepEquals, map[string]interface{}{ + "planoverride": map[string]interface{}{ + "cpuBurst": float64(1.3), + }, + "metadata": map[string]interface{}{}, + }) + return url && method + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + command := AppUpdate{} + command.Flags().Parse(true, []string{"-a", "ble", "--cpu-burst-factor", "0.5"}) + err := command.Run(&context, client) + c.Assert(err, check.NotNil) + c.Assert(err.Error(), check.Equals, "Invalid factor, please use a value greater equal 1") +} + func (s *S) TestAppUpdateWithoutArgs(c *check.C) { var stdout, stderr bytes.Buffer expected := "Please use the -a/--app flag to specify which app you want to update." diff --git a/tsuru/client/plan.go b/tsuru/client/plan.go index ad2c1d4c5..6bcfd0d42 100644 --- a/tsuru/client/plan.go +++ b/tsuru/client/plan.go @@ -18,8 +18,11 @@ import ( ) type PlanList struct { - bytes bool - fs *gnuflag.FlagSet + bytes bool + k8sFriendly bool + showMaxBurstAllowed bool + + fs *gnuflag.FlagSet } func (c *PlanList) Flags() *gnuflag.FlagSet { @@ -27,6 +30,9 @@ func (c *PlanList) Flags() *gnuflag.FlagSet { c.fs = gnuflag.NewFlagSet("plan-list", gnuflag.ExitOnError) bytes := "bytesized units for memory and swap." c.fs.BoolVar(&c.bytes, "bytes", false, bytes) + c.fs.BoolVar(&c.showMaxBurstAllowed, "show-max-cpu-burst-allowed", false, "show column about max CPU burst allowed by plan") + c.fs.BoolVar(&c.k8sFriendly, "kubernetes-friendly", false, "show values friendly for a kubernetes user") + c.fs.BoolVar(&c.bytes, "b", false, bytes) } return c.fs @@ -35,23 +41,44 @@ func (c *PlanList) Flags() *gnuflag.FlagSet { func (c *PlanList) Info() *cmd.Info { return &cmd.Info{ Name: "plan-list", - Usage: "plan list [--bytes]", + Usage: "plan list [--bytes][--kubernetes-friendly][--show-max-cpu-burst-allowed]", Desc: "List available plans that can be used when creating an app.", MinArgs: 0, } } -func renderPlans(plans []apptypes.Plan, isBytes, showDefaultColumn bool) string { +type renderPlansOpts struct { + isBytes, showDefaultColumn, showMaxBurstAllowed bool +} + +func renderPlans(plans []apptypes.Plan, opts renderPlansOpts) string { table := tablecli.NewTable() table.Headers = []string{"Name", "CPU", "Memory"} - if showDefaultColumn { + showBurstColumn := false + + for _, p := range plans { + if hasBurst(p) { + showBurstColumn = true + break + } + } + + if showBurstColumn { + table.Headers = append(table.Headers, "CPU Burst (default)") + } + + if showBurstColumn && opts.showMaxBurstAllowed { + table.Headers = append(table.Headers, "CPU Burst (max customizable)") + } + + if opts.showDefaultColumn { table.Headers = append(table.Headers, "Default") } for _, p := range plans { var cpu, memory string - if isBytes { + if opts.isBytes { memory = fmt.Sprintf("%d", p.Memory) } else { memory = resource.NewQuantity(p.Memory, resource.BinarySI).String() @@ -73,7 +100,22 @@ func renderPlans(plans []apptypes.Plan, isBytes, showDefaultColumn bool) string memory, } - if showDefaultColumn { + if showBurstColumn { + cpuBurst := p.CPUBurst.Default + cpuBurstObservation := "" + if p.Override.CPUBurst != nil { + cpuBurst = *p.Override.CPUBurst + cpuBurstObservation = " (override)" + } + + row = append(row, displayCPUBurst(p.CPUMilli, cpuBurst)+cpuBurstObservation) + } + + if showBurstColumn && opts.showMaxBurstAllowed { + row = append(row, displayCPUBurst(p.CPUMilli, p.CPUBurst.MaxAllowed)) + } + + if opts.showDefaultColumn { row = append(row, strconv.FormatBool(p.Default)) } table.AddRow(row) @@ -81,6 +123,84 @@ func renderPlans(plans []apptypes.Plan, isBytes, showDefaultColumn bool) string return table.String() } +func renderPlansK8SFriendly(plans []apptypes.Plan, showMaxBurstAllowed bool) string { + table := tablecli.NewTable() + table.Headers = []string{"Name"} + + showCPULimitsColumn := false + for _, p := range plans { + if hasBurst(p) { + showCPULimitsColumn = true + break + } + } + + if showCPULimitsColumn { + table.Headers = append(table.Headers, "CPU requests", "CPU limits") + } else { + table.Headers = append(table.Headers, "CPU requests/limits") + } + + if showMaxBurstAllowed { + table.Headers = append(table.Headers, "CPU limits (max customizable)") + } + + table.Headers = append(table.Headers, "Memory requests/limits", "Default") + + for _, p := range plans { + memory := resource.NewQuantity(p.Memory, resource.BinarySI).String() + cpuRequest := resource.NewMilliQuantity(int64(p.CPUMilli), resource.DecimalSI).String() + maxCPULimit := resource.NewMilliQuantity(int64(float64(p.CPUMilli)*p.CPUBurst.MaxAllowed), resource.DecimalSI).String() + + row := []string{ + p.Name, + } + + if showCPULimitsColumn { + cpuBurst := p.CPUBurst.Default + if cpuBurst < 1 { + cpuBurst = 1 + } + defaultCPULimit := resource.NewMilliQuantity(int64(float64(p.CPUMilli)*cpuBurst), resource.DecimalSI).String() + row = append(row, cpuRequest, defaultCPULimit) + } else { + row = append(row, cpuRequest) + } + + if showMaxBurstAllowed { + row = append(row, maxCPULimit) + } + + row = append(row, memory, strconv.FormatBool(p.Default)) + + table.AddRow(row) + } + return table.String() +} + +func hasBurst(p apptypes.Plan) bool { + if p.CPUMilli == 0 { + return false + } + if p.CPUBurst.Default != 0 { + return true + } + + if p.Override.CPUBurst != nil { + return true + } + return false +} + +func displayCPUBurst(currentCPU int, burst float64) string { + if currentCPU == 0 || burst < 1 { + return "" + } + + cpu := int(float64(currentCPU) * burst / 10) + return fmt.Sprintf("up to %g", float64(cpu)) + "%" +} + func (c *PlanList) Run(context *cmd.Context, client *cmd.Client) error { url, err := cmd.GetURL("/plans") if err != nil { @@ -104,6 +224,12 @@ func (c *PlanList) Run(context *cmd.Context, client *cmd.Client) error { if err != nil { return err } - fmt.Fprintf(context.Stdout, "%s", renderPlans(plans, c.bytes, true)) + + if c.k8sFriendly { + fmt.Fprintf(context.Stdout, "%s", renderPlansK8SFriendly(plans, c.showMaxBurstAllowed)) + } else { + fmt.Fprintf(context.Stdout, "%s", renderPlans(plans, renderPlansOpts{isBytes: c.bytes, showDefaultColumn: true, showMaxBurstAllowed: c.showMaxBurstAllowed})) + } + return nil } diff --git a/tsuru/client/plan_test.go b/tsuru/client/plan_test.go index 6fadcc80d..456bd2576 100644 --- a/tsuru/client/plan_test.go +++ b/tsuru/client/plan_test.go @@ -76,7 +76,35 @@ func (s *S) TestPlanListHuman(c *check.C) { } client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) command := PlanList{} - // command.Flags().Parse(true, []string{"-h"}) + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(stdout.String(), check.Equals, expected) +} + +func (s *S) TestPlanListKubernetesFriendly(c *check.C) { + var stdout, stderr bytes.Buffer + result := `[ + {"name": "test", "cpumilli": 300, "memory": 536870912, "default": false} +]` + expected := `+------+---------------------+------------------------+---------+ +| Name | CPU requests/limits | Memory requests/limits | Default | ++------+---------------------+------------------------+---------+ +| test | 300m | 512Mi | false | ++------+---------------------+------------------------+---------+ +` + context := cmd.Context{ + Args: []string{}, + Stdout: &stdout, + Stderr: &stderr, + } + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Message: string(result), Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + return strings.HasSuffix(req.URL.Path, "/plans") && req.Method == "GET" + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + command := PlanList{k8sFriendly: true} err := command.Run(&context, client) c.Assert(err, check.IsNil) c.Assert(stdout.String(), check.Equals, expected) @@ -106,7 +134,124 @@ func (s *S) TestPlanListOverride(c *check.C) { } client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) command := PlanList{} - // command.Flags().Parse(true, []string{"-h"}) + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(stdout.String(), check.Equals, expected) +} + +func (s *S) TestPlanListWithBurst(c *check.C) { + var stdout, stderr bytes.Buffer + result := `[ + {"name": "test", "cpumilli": 300, "memory": 536870912, "default": false, "cpuBurst": {"default": 1.1}} +]` + expected := `+------+-----+--------+---------------------+---------+ +| Name | CPU | Memory | CPU Burst (default) | Default | ++------+-----+--------+---------------------+---------+ +| test | 30% | 512Mi | up to 33% | false | ++------+-----+--------+---------------------+---------+ +` + context := cmd.Context{ + Args: []string{}, + Stdout: &stdout, + Stderr: &stderr, + } + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Message: string(result), Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + return strings.HasSuffix(req.URL.Path, "/plans") && req.Method == "GET" + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + command := PlanList{} + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(stdout.String(), check.Equals, expected) +} + +func (s *S) TestPlanListWithBurstKubernetesFriendly(c *check.C) { + var stdout, stderr bytes.Buffer + result := `[ + {"name": "test", "cpumilli": 300, "memory": 536870912, "default": false, "cpuBurst": {"default": 1.1}} +]` + expected := `+------+--------------+------------+------------------------+---------+ +| Name | CPU requests | CPU limits | Memory requests/limits | Default | ++------+--------------+------------+------------------------+---------+ +| test | 300m | 330m | 512Mi | false | ++------+--------------+------------+------------------------+---------+ +` + context := cmd.Context{ + Args: []string{}, + Stdout: &stdout, + Stderr: &stderr, + } + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Message: string(result), Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + return strings.HasSuffix(req.URL.Path, "/plans") && req.Method == "GET" + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + command := PlanList{k8sFriendly: true} + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(stdout.String(), check.Equals, expected) +} + +func (s *S) TestPlanListWithBurstAndMaxAllowed(c *check.C) { + var stdout, stderr bytes.Buffer + result := `[ + {"name": "test", "cpumilli": 300, "memory": 536870912, "default": false, "cpuBurst": {"default": 1.1, "maxAllowed": 2}} +]` + expected := `+------+-----+--------+---------------------+------------------------------+---------+ +| Name | CPU | Memory | CPU Burst (default) | CPU Burst (max customizable) | Default | ++------+-----+--------+---------------------+------------------------------+---------+ +| test | 30% | 512Mi | up to 33% | up to 60% | false | ++------+-----+--------+---------------------+------------------------------+---------+ +` + context := cmd.Context{ + Args: []string{}, + Stdout: &stdout, + Stderr: &stderr, + } + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Message: string(result), Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + return strings.HasSuffix(req.URL.Path, "/plans") && req.Method == "GET" + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + command := PlanList{ + showMaxBurstAllowed: true, + } + err := command.Run(&context, client) + c.Assert(err, check.IsNil) + c.Assert(stdout.String(), check.Equals, expected) +} + +func (s *S) TestPlanListWithBurstOverride(c *check.C) { + var stdout, stderr bytes.Buffer + result := `[ + {"name": "test", "cpumilli": 300, "memory": 536870912, "default": false, "cpuBurst": {"default": 1.1}, "override": {"cpuBurst": 1.2}} +]` + expected := `+------+-----+--------+----------------------+---------+ +| Name | CPU | Memory | CPU Burst (default) | Default | ++------+-----+--------+----------------------+---------+ +| test | 30% | 512Mi | up to 36% (override) | false | ++------+-----+--------+----------------------+---------+ +` + context := cmd.Context{ + Args: []string{}, + Stdout: &stdout, + Stderr: &stderr, + } + trans := &cmdtest.ConditionalTransport{ + Transport: cmdtest.Transport{Message: string(result), Status: http.StatusOK}, + CondFunc: func(req *http.Request) bool { + return strings.HasSuffix(req.URL.Path, "/plans") && req.Method == "GET" + }, + } + client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) + command := PlanList{} err := command.Run(&context, client) c.Assert(err, check.IsNil) c.Assert(stdout.String(), check.Equals, expected) @@ -136,7 +281,6 @@ func (s *S) TestPlanListCPUMilli(c *check.C) { } client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) command := PlanList{} - // command.Flags().Parse(true, []string{"-h"}) err := command.Run(&context, client) c.Assert(err, check.IsNil) c.Assert(stdout.String(), check.Equals, expected)