Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow configuring default sort columns for each supported resource #795

Merged
merged 6 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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