Skip to content

Commit

Permalink
feat: allow configuring default sort columns for each supported resou…
Browse files Browse the repository at this point in the history
…rce (#795)

This PR adds options to configure the default sorting column for each
resource that supports the `list` command.
See #418 and #434
  • Loading branch information
phm07 committed Jul 10, 2024
1 parent 320300c commit f6877a1
Show file tree
Hide file tree
Showing 25 changed files with 253 additions and 30 deletions.
21 changes: 19 additions & 2 deletions internal/cmd/base/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package base

import (
"fmt"
"os"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand All @@ -10,11 +11,13 @@ import (
"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
)

// ListCmd allows defining commands for listing resources
type ListCmd struct {
SortOption *config.Option[[]string]
ResourceNamePlural string // e.g. "servers"
JSONKeyGetByName string // e.g. "servers"
DefaultColumns []string
Expand Down Expand Up @@ -48,7 +51,7 @@ func (lc *ListCmd) CobraCommand(s state.State) *cobra.Command {
if lc.AdditionalFlags != nil {
lc.AdditionalFlags(cmd)
}
cmd.Flags().StringSliceP("sort", "s", []string{"id:asc"}, "Determine the sorting of the result")
cmd.Flags().StringSliceP("sort", "s", []string{}, "Determine the sorting of the result")
return cmd
}

Expand All @@ -61,7 +64,21 @@ func (lc *ListCmd) Run(s state.State, cmd *cobra.Command) error {
LabelSelector: labelSelector,
PerPage: 50,
}
sorts, _ := cmd.Flags().GetStringSlice("sort")

var sorts []string
if cmd.Flags().Changed("sort") {
if lc.SortOption == nil {
_, _ = fmt.Fprintln(os.Stderr, "Warning: resource does not support sorting. Ignoring --sort flag.")
} else {
sorts, _ = cmd.Flags().GetStringSlice("sort")
}
} else if lc.SortOption != nil {
var err error
sorts, err = lc.SortOption.Get(s.Config())
if err != nil {
return err
}
}

resources, err := lc.Fetch(s, cmd.Flags(), listOpts, sorts)
if err != nil {
Expand Down
83 changes: 70 additions & 13 deletions internal/cmd/base/list_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package base_test

import (
"cmp"
"fmt"
"slices"
"testing"

"github.com/spf13/pflag"

"github.com/hetznercloud/cli/internal/cmd/base"
"github.com/hetznercloud/cli/internal/cmd/output"
"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/cli/internal/testutil"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
)
Expand All @@ -36,30 +40,62 @@ var fakeListCmd = &base.ListCmd{

DefaultColumns: []string{"id", "name"},

Fetch: func(s state.State, set *pflag.FlagSet, opts hcloud.ListOpts, strings []string) ([]interface{}, error) {
return []interface{}{
&fakeResource{
Fetch: func(s state.State, set *pflag.FlagSet, opts hcloud.ListOpts, sort []string) ([]interface{}, error) {
resources := []*fakeResource{
{
ID: 456,
Name: "test2",
},
{
ID: 123,
Name: "test",
},
&fakeResource{
ID: 321,
Name: "test2",
},
&fakeResource{
ID: 42,
{
ID: 789,
Name: "test3",
},
}, nil
}
if len(sort) > 0 {
switch sort[0] {
case "id:asc":
slices.SortFunc(resources, func(a, b *fakeResource) int {
return cmp.Compare(a.ID, b.ID)
})
case "id:desc":
slices.SortFunc(resources, func(a, b *fakeResource) int {
return cmp.Compare(b.ID, a.ID)
})
case "name:asc":
slices.SortFunc(resources, func(a, b *fakeResource) int {
return cmp.Compare(a.Name, b.Name)
})
case "name:desc":
slices.SortFunc(resources, func(a, b *fakeResource) int {
return cmp.Compare(b.Name, a.Name)
})
}
}
return util.ToAnySlice(resources), nil
},
}

func TestList(t *testing.T) {
const resourceSchema = `[{"id": 123, "name": "test"}, {"id": 321, "name": "test2"}, {"id": 42, "name": "test3"}]`
sortOpt, cleanup := config.NewTestOption(
"sort.fakeresource",
"",
[]string{"id:asc"},
(config.DefaultPreferenceFlags&^config.OptionFlagPFlag)|config.OptionFlagSlice,
nil,
)
defer cleanup()

fakeListCmd.SortOption = sortOpt

const resourceSchema = `[{"id": 123, "name": "test"}, {"id": 456, "name": "test2"}, {"id": 789, "name": "test3"}]`
testutil.TestCommand(t, fakeListCmd, map[string]testutil.TestCase{
"no flags": {
Args: []string{"list"},
ExpOut: "ID NAME\n123 test\n321 test2\n42 test3\n",
ExpOut: "ID NAME\n123 test\n456 test2\n789 test3\n",
},
"json": {
Args: []string{"list", "-o=json"},
Expand All @@ -73,7 +109,7 @@ func TestList(t *testing.T) {
},
"quiet": {
Args: []string{"list", "--quiet"},
ExpOut: "ID NAME\n123 test\n321 test2\n42 test3\n",
ExpOut: "ID NAME\n123 test\n456 test2\n789 test3\n",
},
"json quiet": {
Args: []string{"list", "-o=json", "--quiet"},
Expand All @@ -85,5 +121,26 @@ func TestList(t *testing.T) {
ExpOut: resourceSchema,
ExpOutType: testutil.DataTypeYAML,
},
"sort": {
Args: []string{"list", "--sort", "id:desc", "-o=json"},
ExpOut: `[{"id": 789, "name": "test3"}, {"id": 456, "name": "test2"}, {"id": 123, "name": "test"}]`,
ExpOutType: testutil.DataTypeJSON,
},
"no sort": {
Args: []string{"list", "--sort=", "-o=json"},
ExpOut: `[{"id": 456, "name": "test2"}, {"id": 123, "name": "test"}, {"id": 789, "name": "test3"}]`,
ExpOutType: testutil.DataTypeJSON,
},
"sort with option": {
Args: []string{"list", "-o=json"},
PreRun: func(t *testing.T, fx *testutil.Fixture) {
sortOpt.Override(fx.Config, []string{"id:desc"})
t.Cleanup(func() {
sortOpt.Override(fx.Config, nil)
})
},
ExpOut: `[{"id": 789, "name": "test3"}, {"id": 456, "name": "test2"}, {"id": 123, "name": "test"}]`,
ExpOutType: testutil.DataTypeJSON,
},
})
}
2 changes: 2 additions & 0 deletions internal/cmd/certificate/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)
Expand All @@ -19,6 +20,7 @@ var ListCmd = base.ListCmd{
ResourceNamePlural: "Certificates",
JSONKeyGetByName: "certificates",
DefaultColumns: []string{"id", "name", "type", "domain_names", "not_valid_after", "age"},
SortOption: config.OptionSortCertificate,

Fetch: func(s state.State, _ *pflag.FlagSet, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
opts := hcloud.CertificateListOpts{ListOpts: listOpts}
Expand Down
21 changes: 17 additions & 4 deletions internal/cmd/config/helptext/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@ import (
//go:generate go run $GOFILE

func main() {
generateTable("preferences.txt", config.OptionFlagPreference, true)
generateTable("other.txt", config.OptionFlagPreference, false)
generateTable(
"preferences.txt",
config.OptionFlagPreference|config.OptionFlagHidden,
config.OptionFlagPreference,
table.Row{"sort.<resource>", "Default sorting for resource", "string list", "sort.<resource>", "HCLOUD_SORT_<RESOURCE>", ""},
)
generateTable("other.txt",
config.OptionFlagPreference|config.OptionFlagHidden,
0,
)
}

func generateTable(outFile string, filterFlag config.OptionFlag, hasFlag bool) {
func generateTable(outFile string, mask, filter config.OptionFlag, extraRows ...table.Row) {
f, err := os.OpenFile(outFile, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
Expand All @@ -46,7 +54,7 @@ func generateTable(outFile string, filterFlag config.OptionFlag, hasFlag bool) {

var opts []config.IOption
for _, opt := range config.Options {
if opt.HasFlags(filterFlag) != hasFlag {
if opt.GetFlags()&mask != filter {
continue
}
opts = append(opts, opt)
Expand All @@ -61,6 +69,11 @@ func generateTable(outFile string, filterFlag config.OptionFlag, hasFlag bool) {
t.AppendSeparator()
}

for _, row := range extraRows {
t.AppendRow(row)
t.AppendSeparator()
}

t.Render()
}

Expand Down
3 changes: 3 additions & 0 deletions internal/cmd/config/helptext/preferences.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@
├──────────────────┼──────────────────────┼─────────────┼──────────────────┼─────────────────────────┼─────────────────┤
│ quiet │ If true, only print │ boolean │ quiet │ HCLOUD_QUIET │ --quiet │
│ │ error messages │ │ │ │ │
├──────────────────┼──────────────────────┼─────────────┼──────────────────┼─────────────────────────┼─────────────────┤
│ sort.<resource> │ Default sorting for │ string list │ sort.<resource> │ HCLOUD_SORT_<RESOURCE> │ │
│ │ resource │ │ │ │ │
└──────────────────┴──────────────────────┴─────────────┴──────────────────┴─────────────────────────┴─────────────────┘
2 changes: 2 additions & 0 deletions internal/cmd/datacenter/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hetznercloud/cli/internal/cmd/output"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)
Expand All @@ -15,6 +16,7 @@ var ListCmd = base.ListCmd{
ResourceNamePlural: "Datacenters",
JSONKeyGetByName: "datacenters",
DefaultColumns: []string{"id", "name", "description", "location"},
SortOption: config.OptionSortDatacenter,

Fetch: func(s state.State, _ *pflag.FlagSet, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
opts := hcloud.DatacenterListOpts{ListOpts: listOpts}
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/firewall/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/hetznercloud/cli/internal/cmd/output"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)
Expand All @@ -17,6 +18,7 @@ var ListCmd = base.ListCmd{
ResourceNamePlural: "Firewalls",
JSONKeyGetByName: "firewalls",
DefaultColumns: []string{"id", "name", "rules_count", "applied_to_count"},
SortOption: config.OptionSortFirewall,

Fetch: func(s state.State, _ *pflag.FlagSet, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
opts := hcloud.FirewallListOpts{ListOpts: listOpts}
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/floatingip/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)
Expand All @@ -20,6 +21,7 @@ var ListCmd = base.ListCmd{
ResourceNamePlural: "Floating IPs",
JSONKeyGetByName: "floating_ips",
DefaultColumns: []string{"id", "type", "name", "description", "ip", "home", "server", "dns", "age"},
SortOption: config.OptionSortFloatingIP,

Fetch: func(s state.State, _ *pflag.FlagSet, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
opts := hcloud.FloatingIPListOpts{ListOpts: listOpts}
Expand Down
3 changes: 3 additions & 0 deletions internal/cmd/image/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)
Expand All @@ -23,6 +24,8 @@ var ListCmd = base.ListCmd{
ResourceNamePlural: "Images",
JSONKeyGetByName: "images",
DefaultColumns: []string{"id", "type", "name", "description", "architecture", "image_size", "disk_size", "created", "deprecated"},
SortOption: config.OptionSortImage,

AdditionalFlags: func(cmd *cobra.Command) {
cmd.Flags().StringSliceP("type", "t", []string{}, "Only show images of given type: system|app|snapshot|backup")
cmd.RegisterFlagCompletionFunc("type", cmpl.SuggestCandidates("backup", "snapshot", "system", "app"))
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/iso/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ var ListCmd = base.ListCmd{
ResourceNamePlural: "ISOs",
JSONKeyGetByName: "isos",
DefaultColumns: []string{"id", "name", "description", "type", "architecture"},
SortOption: nil, // ISOs does not support sorting

AdditionalFlags: func(cmd *cobra.Command) {
cmd.Flags().StringSlice("architecture", []string{}, "Only show images of given architecture: x86|arm")
cmd.RegisterFlagCompletionFunc("architecture", cmpl.SuggestCandidates(string(hcloud.ArchitectureX86), string(hcloud.ArchitectureARM)))
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/iso/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestList(t *testing.T) {
gomock.Any(),
hcloud.ISOListOpts{
ListOpts: hcloud.ListOpts{PerPage: 50},
Sort: []string{"id:asc"},
Sort: nil, // ISOs do not support sorting
},
).
Return([]*hcloud.ISO{
Expand Down
3 changes: 3 additions & 0 deletions internal/cmd/loadbalancer/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/hetznercloud/cli/internal/cmd/util"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)
Expand All @@ -19,6 +20,8 @@ var ListCmd = base.ListCmd{
ResourceNamePlural: "Load Balancer",
JSONKeyGetByName: "load_balancers",
DefaultColumns: []string{"id", "name", "health", "ipv4", "ipv6", "type", "location", "network_zone", "age"},
SortOption: config.OptionSortLoadBalancer,

Fetch: func(s state.State, _ *pflag.FlagSet, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
opts := hcloud.LoadBalancerListOpts{ListOpts: listOpts}
if len(sorts) > 0 {
Expand Down
4 changes: 2 additions & 2 deletions internal/cmd/loadbalancertype/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (
var ListCmd = base.ListCmd{
ResourceNamePlural: "Load Balancer Types",
JSONKeyGetByName: "load_balancer_types",

DefaultColumns: []string{"id", "name", "description", "max_services", "max_connections", "max_targets"},
DefaultColumns: []string{"id", "name", "description", "max_services", "max_connections", "max_targets"},
SortOption: nil, // Load Balancer Types do not support sorting

Fetch: func(s state.State, _ *pflag.FlagSet, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
opts := hcloud.LoadBalancerTypeListOpts{ListOpts: listOpts}
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/loadbalancertype/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestList(t *testing.T) {
gomock.Any(),
hcloud.LoadBalancerTypeListOpts{
ListOpts: hcloud.ListOpts{PerPage: 50},
Sort: []string{"id:asc"},
Sort: nil, // Load Balancer Types do not support sorting
},
).
Return([]*hcloud.LoadBalancerType{
Expand Down
2 changes: 2 additions & 0 deletions internal/cmd/location/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/hetznercloud/cli/internal/cmd/output"
"github.com/hetznercloud/cli/internal/hcapi2"
"github.com/hetznercloud/cli/internal/state"
"github.com/hetznercloud/cli/internal/state/config"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/hetznercloud/hcloud-go/v2/hcloud/schema"
)
Expand All @@ -15,6 +16,7 @@ var ListCmd = base.ListCmd{
ResourceNamePlural: "locations",
JSONKeyGetByName: "locations",
DefaultColumns: []string{"id", "name", "description", "network_zone", "country", "city"},
SortOption: config.OptionSortLocation,

Fetch: func(s state.State, _ *pflag.FlagSet, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
opts := hcloud.LocationListOpts{ListOpts: listOpts}
Expand Down
1 change: 1 addition & 0 deletions internal/cmd/network/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var ListCmd = base.ListCmd{
ResourceNamePlural: "Networks",
JSONKeyGetByName: "networks",
DefaultColumns: []string{"id", "name", "ip_range", "servers", "age"},
SortOption: nil, // Networks do not support sorting

Fetch: func(s state.State, _ *pflag.FlagSet, listOpts hcloud.ListOpts, sorts []string) ([]interface{}, error) {
opts := hcloud.NetworkListOpts{ListOpts: listOpts}
Expand Down
Loading

0 comments on commit f6877a1

Please sign in to comment.