diff --git a/CHANGELOG.md b/CHANGELOG.md index 8986002cf..2f6389ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## Added +### Added - Add `gateway plans` command for listing gateway plans. +### Changed + +- In all outputs of `server plans`, sort plans by CPU count, memory amount, and storage size. +- In human readable output of `server plans`, group plans by type. + ## [3.8.1] - 2024-05-24 ### Changed diff --git a/internal/commands/server/firewall/show_test.go b/internal/commands/server/firewall/show_test.go index 316acee93..118799a89 100644 --- a/internal/commands/server/firewall/show_test.go +++ b/internal/commands/server/firewall/show_test.go @@ -143,7 +143,8 @@ func TestFirewallShowHumanOutput(t *testing.T) { }, } - expected := ` Firewall rules + expected := ` + Firewall rules # Action Source Destination Dir Proto ─── ──────── ─────────────── ───────────── ───── ────────── diff --git a/internal/commands/server/plan_list.go b/internal/commands/server/plan_list.go index e615605e5..b13661020 100644 --- a/internal/commands/server/plan_list.go +++ b/internal/commands/server/plan_list.go @@ -1,8 +1,12 @@ package server import ( + "sort" + "strings" + "github.com/UpCloudLtd/upcloud-cli/v3/internal/commands" "github.com/UpCloudLtd/upcloud-cli/v3/internal/output" + "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud" ) // PlanListCommand creates the "server plans" command @@ -18,14 +22,28 @@ type planListCommand struct { // ExecuteWithoutArguments implements commands.NoArgumentCommand func (s *planListCommand) ExecuteWithoutArguments(exec commands.Executor) (output.Output, error) { - plans, err := exec.All().GetPlans(exec.Context()) + plansObj, err := exec.All().GetPlans(exec.Context()) if err != nil { return nil, err } - rows := []output.TableRow{} - for _, p := range plans.Plans { - rows = append(rows, output.TableRow{ + plans := plansObj.Plans + sort.Slice(plans, func(i, j int) bool { + if plans[i].CoreNumber != plans[j].CoreNumber { + return plans[i].CoreNumber < plans[j].CoreNumber + } + + if plans[i].MemoryAmount != plans[j].MemoryAmount { + return plans[i].MemoryAmount < plans[j].MemoryAmount + } + + return plans[i].StorageSize < plans[j].StorageSize + }) + + rows := make(map[string][]output.TableRow) + for _, p := range plans { + key := planType(p) + rows[key] = append(rows[key], output.TableRow{ p.Name, p.CoreNumber, p.MemoryAmount, @@ -37,7 +55,33 @@ func (s *planListCommand) ExecuteWithoutArguments(exec commands.Executor) (outpu return output.MarshaledWithHumanOutput{ Value: plans, - Output: output.Table{ + Output: output.Combined{ + planSection("general_purpose", "General purpose", rows["general_purpose"]), + planSection("high_cpu", "High CPU", rows["high_cpu"]), + planSection("high_memory", "High memory", rows["high_memory"]), + planSection("developer", "Developer", rows["developer"]), + }, + }, nil +} + +func planType(p upcloud.Plan) string { + if strings.HasPrefix(p.Name, "DEV-") { + return "developer" + } + if strings.HasPrefix(p.Name, "HICPU-") { + return "high_cpu" + } + if strings.HasPrefix(p.Name, "HIMEM-") { + return "high_memory" + } + return "general_purpose" +} + +func planSection(key, title string, rows []output.TableRow) output.CombinedSection { + return output.CombinedSection{ + Key: key, + Title: title, + Contents: output.Table{ Columns: []output.TableColumn{ {Key: "name", Header: "Name"}, {Key: "cores", Header: "Cores"}, @@ -48,5 +92,5 @@ func (s *planListCommand) ExecuteWithoutArguments(exec commands.Executor) (outpu }, Rows: rows, }, - }, nil + } } diff --git a/internal/output/combined.go b/internal/output/combined.go index a233c6ae1..ca09bcae1 100644 --- a/internal/output/combined.go +++ b/internal/output/combined.go @@ -75,8 +75,14 @@ func (m Combined) MarshalHuman() ([]byte, error) { marshaled = prefixLines(marshaled, " ") } out = append(out, marshaled...) + + // ensure newline before first section + if i == 0 && firstNonSpaceChar(out) != '\n' { + out = append([]byte("\n"), out...) + } + + // dont add newline after the last section if i < len(m)-1 { - // dont add newline after the last section out = append(out, []byte("\n")...) } } @@ -84,6 +90,15 @@ func (m Combined) MarshalHuman() ([]byte, error) { return out, nil } +func firstNonSpaceChar(bytes []byte) byte { + for _, b := range bytes { + if b != ' ' { + return b + } + } + return 0 +} + func prefixLines(marshaled []byte, s string) (out []byte) { padding := []byte(s) for _, b := range marshaled { diff --git a/internal/output/combined_test.go b/internal/output/combined_test.go index 5eccf591c..d42f2b725 100644 --- a/internal/output/combined_test.go +++ b/internal/output/combined_test.go @@ -69,7 +69,8 @@ func TestCombined(t *testing.T) { }, }}, }, - expectedHumanResult: ` MOCK + expectedHumanResult: ` + MOCK B D ─── ────