From b9443c86a0f498f8d9629c5e8efe214abe89ea89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Tue, 10 Oct 2023 15:21:02 -0300 Subject: [PATCH 1/6] plan: Add support to display CPU Burst --- go.mod | 4 +- go.sum | 6 +++ tsuru/client/apps.go | 20 +++++++- tsuru/client/plan.go | 104 ++++++++++++++++++++++++++++++++++++-- tsuru/client/plan_test.go | 92 +++++++++++++++++++++++++++++++++ 5 files changed, 217 insertions(+), 9 deletions(-) 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..dfbdf0251 100644 --- a/go.sum +++ b/go.sum @@ -741,6 +741,12 @@ github.com/tsuru/tablecli v0.0.0-20190131152944-7ded8a3383c6 h1:1XDdWFAjIbCSG1Oj 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/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-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..d6bd6ebdd 100644 --- a/tsuru/client/apps.go +++ b/tsuru/client/apps.go @@ -219,7 +219,7 @@ type AppUpdate struct { cmd.AppNameMixIn cmd.ConfirmationCommand - memory, cpu string + memory, cpu, cpuBurst string } func (c *AppUpdate) Info() *cmd.Info { @@ -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 CPU burst, when value is 1 not set 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 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}, false, false, false)) } if !simplified && internalAddressesTable.Rows() > 0 { buf.WriteString("\n") diff --git a/tsuru/client/plan.go b/tsuru/client/plan.go index ad2c1d4c5..e3d486ead 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,16 +41,41 @@ 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]", Desc: "List available plans that can be used when creating an app.", MinArgs: 0, } } -func renderPlans(plans []apptypes.Plan, isBytes, showDefaultColumn bool) string { +func renderPlans(plans []apptypes.Plan, isBytes, showDefaultColumn bool, showMaxBurstAllowed bool) string { table := tablecli.NewTable() table.Headers = []string{"Name", "CPU", "Memory"} + showBurstColumn := false + + for _, p := range plans { + if p.CPUMilli == 0 { + continue + } + if p.CPUBurst.Default != 0 { + showBurstColumn = true + break + } + + if p.Override.CPUBurst != nil { + showBurstColumn = true + break + } + } + + if showBurstColumn { + table.Headers = append(table.Headers, "CPU Burst (default)") + } + + if showBurstColumn && showMaxBurstAllowed { + table.Headers = append(table.Headers, "CPU Burst (max customizable)") + } + if showDefaultColumn { table.Headers = append(table.Headers, "Default") } @@ -73,6 +104,21 @@ func renderPlans(plans []apptypes.Plan, isBytes, showDefaultColumn bool) string memory, } + 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 && showMaxBurstAllowed { + row = append(row, displayCPUBurst(p.CPUMilli, p.CPUBurst.MaxAllowed)) + } + if showDefaultColumn { row = append(row, strconv.FormatBool(p.Default)) } @@ -81,6 +127,48 @@ 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", "CPU requests", "CPU 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() + defaultCPULimit := resource.NewMilliQuantity(int64(float64(p.CPUMilli)*p.CPUBurst.Default), resource.DecimalSI).String() + maxCPULimit := resource.NewMilliQuantity(int64(float64(p.CPUMilli)*p.CPUBurst.MaxAllowed), resource.DecimalSI).String() + + row := []string{ + p.Name, + cpuRequest, + defaultCPULimit, + } + + if showMaxBurstAllowed { + row = append(row, maxCPULimit) + } + + row = append(row, memory, strconv.FormatBool(p.Default)) + + table.AddRow(row) + } + return table.String() +} + +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 +192,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, c.bytes, true, c.showMaxBurstAllowed)) + } + return nil } diff --git a/tsuru/client/plan_test.go b/tsuru/client/plan_test.go index 6fadcc80d..dcc8fd131 100644 --- a/tsuru/client/plan_test.go +++ b/tsuru/client/plan_test.go @@ -112,6 +112,98 @@ func (s *S) TestPlanListOverride(c *check.C) { 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{} + // 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) 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, + } + // 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) 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{} + // 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) TestPlanListCPUMilli(c *check.C) { var stdout, stderr bytes.Buffer result := `[ From 576045badb87fa613065c0c68281334291083ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Wed, 11 Oct 2023 09:46:22 -0300 Subject: [PATCH 2/6] plan: Improve k8s friendly table --- go.sum | 8 ------ tsuru/client/plan.go | 54 ++++++++++++++++++++++++++--------- tsuru/client/plan_test.go | 60 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 21 deletions(-) diff --git a/go.sum b/go.sum index dfbdf0251..a37e8a33a 100644 --- a/go.sum +++ b/go.sum @@ -733,14 +733,6 @@ 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/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/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= diff --git a/tsuru/client/plan.go b/tsuru/client/plan.go index e3d486ead..d8094b5e3 100644 --- a/tsuru/client/plan.go +++ b/tsuru/client/plan.go @@ -54,15 +54,7 @@ func renderPlans(plans []apptypes.Plan, isBytes, showDefaultColumn bool, showMax showBurstColumn := false for _, p := range plans { - if p.CPUMilli == 0 { - continue - } - if p.CPUBurst.Default != 0 { - showBurstColumn = true - break - } - - if p.Override.CPUBurst != nil { + if hasBurst(p) { showBurstColumn = true break } @@ -129,7 +121,21 @@ func renderPlans(plans []apptypes.Plan, isBytes, showDefaultColumn bool, showMax func renderPlansK8SFriendly(plans []apptypes.Plan, showMaxBurstAllowed bool) string { table := tablecli.NewTable() - table.Headers = []string{"Name", "CPU requests", "CPU limits"} + 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)") @@ -140,13 +146,21 @@ func renderPlansK8SFriendly(plans []apptypes.Plan, showMaxBurstAllowed bool) str for _, p := range plans { memory := resource.NewQuantity(p.Memory, resource.BinarySI).String() cpuRequest := resource.NewMilliQuantity(int64(p.CPUMilli), resource.DecimalSI).String() - defaultCPULimit := resource.NewMilliQuantity(int64(float64(p.CPUMilli)*p.CPUBurst.Default), resource.DecimalSI).String() maxCPULimit := resource.NewMilliQuantity(int64(float64(p.CPUMilli)*p.CPUBurst.MaxAllowed), resource.DecimalSI).String() row := []string{ p.Name, - cpuRequest, - defaultCPULimit, + } + + 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 { @@ -160,6 +174,20 @@ func renderPlansK8SFriendly(plans []apptypes.Plan, showMaxBurstAllowed bool) str 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 "" diff --git a/tsuru/client/plan_test.go b/tsuru/client/plan_test.go index dcc8fd131..2ed7f986c 100644 --- a/tsuru/client/plan_test.go +++ b/tsuru/client/plan_test.go @@ -82,6 +82,36 @@ func (s *S) TestPlanListHuman(c *check.C) { 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} + // 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) TestPlanListOverride(c *check.C) { var stdout, stderr bytes.Buffer result := `[ @@ -142,6 +172,36 @@ func (s *S) TestPlanListWithBurst(c *check.C) { 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} + // 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) TestPlanListWithBurstAndMaxAllowed(c *check.C) { var stdout, stderr bytes.Buffer result := `[ From aff0e33dda15bb78a0062c80bf35aec669831f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Wed, 11 Oct 2023 10:25:45 -0300 Subject: [PATCH 3/6] refactor renderPlan to be more clear for use --- tsuru/client/apps.go | 2 +- tsuru/client/plan.go | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tsuru/client/apps.go b/tsuru/client/apps.go index d6bd6ebdd..115b278fc 100644 --- a/tsuru/client/apps.go +++ b/tsuru/client/apps.go @@ -764,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, false)) + buf.WriteString(renderPlans([]apptypes.Plan{a.Plan}, renderPlansOpts{})) } if !simplified && internalAddressesTable.Rows() > 0 { buf.WriteString("\n") diff --git a/tsuru/client/plan.go b/tsuru/client/plan.go index d8094b5e3..189db8048 100644 --- a/tsuru/client/plan.go +++ b/tsuru/client/plan.go @@ -47,7 +47,11 @@ func (c *PlanList) Info() *cmd.Info { } } -func renderPlans(plans []apptypes.Plan, isBytes, showDefaultColumn bool, showMaxBurstAllowed 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"} @@ -64,17 +68,17 @@ func renderPlans(plans []apptypes.Plan, isBytes, showDefaultColumn bool, showMax table.Headers = append(table.Headers, "CPU Burst (default)") } - if showBurstColumn && showMaxBurstAllowed { + if showBurstColumn && opts.showMaxBurstAllowed { table.Headers = append(table.Headers, "CPU Burst (max customizable)") } - if showDefaultColumn { + 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() @@ -107,11 +111,11 @@ func renderPlans(plans []apptypes.Plan, isBytes, showDefaultColumn bool, showMax row = append(row, displayCPUBurst(p.CPUMilli, cpuBurst)+cpuBurstObservation) } - if showBurstColumn && showMaxBurstAllowed { + if showBurstColumn && opts.showMaxBurstAllowed { row = append(row, displayCPUBurst(p.CPUMilli, p.CPUBurst.MaxAllowed)) } - if showDefaultColumn { + if opts.showDefaultColumn { row = append(row, strconv.FormatBool(p.Default)) } table.AddRow(row) @@ -224,7 +228,7 @@ func (c *PlanList) Run(context *cmd.Context, client *cmd.Client) error { if c.k8sFriendly { fmt.Fprintf(context.Stdout, "%s", renderPlansK8SFriendly(plans, c.showMaxBurstAllowed)) } else { - fmt.Fprintf(context.Stdout, "%s", renderPlans(plans, c.bytes, true, c.showMaxBurstAllowed)) + fmt.Fprintf(context.Stdout, "%s", renderPlans(plans, renderPlansOpts{isBytes: c.bytes, showDefaultColumn: true, showMaxBurstAllowed: c.showMaxBurstAllowed})) } return nil From fcc4de3e8aefa5d69d27bed140889fd5445e5de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Wed, 11 Oct 2023 10:30:47 -0300 Subject: [PATCH 4/6] plan: some text adjusts --- tsuru/client/apps.go | 4 ++-- tsuru/client/plan_test.go | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/tsuru/client/apps.go b/tsuru/client/apps.go index 115b278fc..b0c77e375 100644 --- a/tsuru/client/apps.go +++ b/tsuru/client/apps.go @@ -257,7 +257,7 @@ 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 CPU burst, when value is 1 not set burst") + flagSet.StringVar(&c.cpuBurst, "cpu-burst-factor", "", "The multiplier to determine the limits of CPU burst, when the value is 1 not set 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( @@ -306,7 +306,7 @@ func (c *AppUpdate) Run(ctx *cmd.Context, cli *cmd.Client) error { } if cpuBurst < 1 { - return errors.New("Invalid factor, please use value greater equal 1") + return errors.New("Invalid factor, please use a value greater equal 1") } c.args.Planoverride.CpuBurst = &cpuBurst diff --git a/tsuru/client/plan_test.go b/tsuru/client/plan_test.go index 2ed7f986c..456bd2576 100644 --- a/tsuru/client/plan_test.go +++ b/tsuru/client/plan_test.go @@ -76,7 +76,6 @@ 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) @@ -106,7 +105,6 @@ func (s *S) TestPlanListKubernetesFriendly(c *check.C) { } client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) command := PlanList{k8sFriendly: true} - // command.Flags().Parse(true, []string{"-h"}) err := command.Run(&context, client) c.Assert(err, check.IsNil) c.Assert(stdout.String(), check.Equals, expected) @@ -136,7 +134,6 @@ 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) @@ -166,7 +163,6 @@ func (s *S) TestPlanListWithBurst(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) @@ -196,7 +192,6 @@ func (s *S) TestPlanListWithBurstKubernetesFriendly(c *check.C) { } client := cmd.NewClient(&http.Client{Transport: trans}, nil, manager) command := PlanList{k8sFriendly: true} - // command.Flags().Parse(true, []string{"-h"}) err := command.Run(&context, client) c.Assert(err, check.IsNil) c.Assert(stdout.String(), check.Equals, expected) @@ -228,7 +223,6 @@ func (s *S) TestPlanListWithBurstAndMaxAllowed(c *check.C) { command := PlanList{ showMaxBurstAllowed: true, } - // command.Flags().Parse(true, []string{"-h"}) err := command.Run(&context, client) c.Assert(err, check.IsNil) c.Assert(stdout.String(), check.Equals, expected) @@ -258,7 +252,6 @@ func (s *S) TestPlanListWithBurstOverride(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) @@ -288,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) From 611095276cd9ce31a9f59b05c4a28ee438fcdf34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Wed, 11 Oct 2023 11:06:08 -0300 Subject: [PATCH 5/6] plan: Add tests for --cpu-burst-factor --- tsuru/client/apps_test.go | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tsuru/client/apps_test.go b/tsuru/client/apps_test.go index 8951b64bd..5cd27fd87 100644 --- a/tsuru/client/apps_test.go +++ b/tsuru/client/apps_test.go @@ -613,6 +613,40 @@ 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) TestAppUpdateWithoutArgs(c *check.C) { var stdout, stderr bytes.Buffer expected := "Please use the -a/--app flag to specify which app you want to update." From d4781f7f9470b3301d44728236178dd2f2c9bb5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wilson=20J=C3=BAnior?= Date: Mon, 16 Oct 2023 13:57:53 -0300 Subject: [PATCH 6/6] Add adjusts to command helpers --- tsuru/client/apps.go | 4 ++-- tsuru/client/apps_test.go | 33 +++++++++++++++++++++++++++++++++ tsuru/client/plan.go | 2 +- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/tsuru/client/apps.go b/tsuru/client/apps.go index b0c77e375..626cd61f8 100644 --- a/tsuru/client/apps.go +++ b/tsuru/client/apps.go @@ -225,7 +225,7 @@ type AppUpdate struct { 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,7 +257,7 @@ 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 CPU burst, when the value is 1 not set burst") + 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( diff --git a/tsuru/client/apps_test.go b/tsuru/client/apps_test.go index 5cd27fd87..97d061b89 100644 --- a/tsuru/client/apps_test.go +++ b/tsuru/client/apps_test.go @@ -647,6 +647,39 @@ func (s *S) TestAppUpdateWithCPUBurst(c *check.C) { 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 189db8048..6bcfd0d42 100644 --- a/tsuru/client/plan.go +++ b/tsuru/client/plan.go @@ -41,7 +41,7 @@ func (c *PlanList) Flags() *gnuflag.FlagSet { func (c *PlanList) Info() *cmd.Info { return &cmd.Info{ Name: "plan-list", - Usage: "plan list [--bytes][--kubernetes-friendly]", + 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, }