Skip to content

Commit 6d3b064

Browse files
authored
feat: add "all list" command to list all available resources (#580)
As described in #545
1 parent 643669d commit 6d3b064

File tree

21 files changed

+530
-3
lines changed

21 files changed

+530
-3
lines changed

internal/cli/root.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/spf13/cobra"
77

8+
"github.com/hetznercloud/cli/internal/cmd/all"
89
"github.com/hetznercloud/cli/internal/cmd/certificate"
910
"github.com/hetznercloud/cli/internal/cmd/completion"
1011
"github.com/hetznercloud/cli/internal/cmd/context"
@@ -39,6 +40,7 @@ func NewRootCommand(state *state.State, client hcapi2.Client) *cobra.Command {
3940
DisableFlagsInUseLine: true,
4041
}
4142
cmd.AddCommand(
43+
all.NewCommand(state, client),
4244
floatingip.NewCommand(state, client),
4345
image.NewCommand(state, client),
4446
server.NewCommand(state, client),

internal/cmd/all/all.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package all
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
6+
"github.com/hetznercloud/cli/internal/hcapi2"
7+
"github.com/hetznercloud/cli/internal/state"
8+
)
9+
10+
func NewCommand(cli *state.State, client hcapi2.Client) *cobra.Command {
11+
cmd := &cobra.Command{
12+
Use: "all",
13+
Short: "Commands that apply to all resources",
14+
Args: cobra.NoArgs,
15+
TraverseChildren: true,
16+
DisableFlagsInUseLine: true,
17+
}
18+
cmd.AddCommand(
19+
listCmd.CobraCommand(cli.Context, client, cli, cli),
20+
)
21+
return cmd
22+
}

internal/cmd/all/list.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package all
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/spf13/cobra"
10+
"github.com/spf13/pflag"
11+
12+
"github.com/hetznercloud/cli/internal/cmd/base"
13+
"github.com/hetznercloud/cli/internal/cmd/certificate"
14+
"github.com/hetznercloud/cli/internal/cmd/firewall"
15+
"github.com/hetznercloud/cli/internal/cmd/floatingip"
16+
"github.com/hetznercloud/cli/internal/cmd/image"
17+
"github.com/hetznercloud/cli/internal/cmd/iso"
18+
"github.com/hetznercloud/cli/internal/cmd/loadbalancer"
19+
"github.com/hetznercloud/cli/internal/cmd/network"
20+
"github.com/hetznercloud/cli/internal/cmd/output"
21+
"github.com/hetznercloud/cli/internal/cmd/placementgroup"
22+
"github.com/hetznercloud/cli/internal/cmd/primaryip"
23+
"github.com/hetznercloud/cli/internal/cmd/server"
24+
"github.com/hetznercloud/cli/internal/cmd/sshkey"
25+
"github.com/hetznercloud/cli/internal/cmd/volume"
26+
"github.com/hetznercloud/cli/internal/hcapi2"
27+
"github.com/hetznercloud/cli/internal/state"
28+
"github.com/hetznercloud/hcloud-go/v2/hcloud"
29+
)
30+
31+
var allCmds = []base.ListCmd{
32+
server.ListCmd,
33+
image.ListCmd,
34+
placementgroup.ListCmd,
35+
primaryip.ListCmd,
36+
iso.ListCmd,
37+
volume.ListCmd,
38+
loadbalancer.ListCmd,
39+
floatingip.ListCmd,
40+
network.ListCmd,
41+
firewall.ListCmd,
42+
certificate.ListCmd,
43+
sshkey.ListCmd,
44+
}
45+
46+
var listCmd = base.Cmd{
47+
BaseCobraCommand: func(client hcapi2.Client) *cobra.Command {
48+
49+
var resources []string
50+
for _, cmd := range allCmds {
51+
resources = append(resources, " - "+cmd.ResourceNamePlural)
52+
}
53+
54+
cmd := &cobra.Command{
55+
Use: "list FLAGS",
56+
Short: "List all resources in the project",
57+
Long: `List all resources in the project. This does not include static/public resources like locations, public ISOs, etc.
58+
59+
Listed resources are:
60+
` + strings.Join(resources, "\n"),
61+
}
62+
63+
cmd.Flags().StringP("selector", "l", "", "Selector to filter by labels")
64+
65+
cmd.Flags().Bool("paid", false, "Only list resources that cost money")
66+
67+
output.AddFlag(cmd, output.OptionJSON())
68+
69+
return cmd
70+
},
71+
Run: func(ctx context.Context, client hcapi2.Client, actionWaiter state.ActionWaiter, cmd *cobra.Command, args []string) error {
72+
73+
paid, _ := cmd.Flags().GetBool("paid")
74+
labelSelector, _ := cmd.Flags().GetString("selector")
75+
76+
outOpts := output.FlagsForCommand(cmd)
77+
78+
var cmds []base.ListCmd
79+
if paid {
80+
cmds = []base.ListCmd{
81+
server.ListCmd,
82+
image.ListCmd,
83+
primaryip.ListCmd,
84+
volume.ListCmd,
85+
loadbalancer.ListCmd,
86+
floatingip.ListCmd,
87+
}
88+
} else {
89+
cmds = []base.ListCmd{
90+
server.ListCmd,
91+
image.ListCmd,
92+
placementgroup.ListCmd,
93+
primaryip.ListCmd,
94+
iso.ListCmd,
95+
volume.ListCmd,
96+
loadbalancer.ListCmd,
97+
floatingip.ListCmd,
98+
network.ListCmd,
99+
firewall.ListCmd,
100+
certificate.ListCmd,
101+
sshkey.ListCmd,
102+
}
103+
}
104+
105+
type response struct {
106+
result []any
107+
err error
108+
}
109+
responseChs := make([]chan response, len(cmds))
110+
111+
// Start all requests in parallel in order to minimize response time
112+
for i, lc := range cmds {
113+
i, lc := i, lc
114+
ch := make(chan response)
115+
responseChs[i] = ch
116+
117+
go func() {
118+
defer close(ch)
119+
120+
listOpts := hcloud.ListOpts{
121+
LabelSelector: labelSelector,
122+
}
123+
124+
flagSet := pflag.NewFlagSet(lc.JSONKeyGetByName, pflag.ExitOnError)
125+
126+
switch lc.JSONKeyGetByName {
127+
case image.ListCmd.JSONKeyGetByName:
128+
flagSet.StringSlice("type", []string{"backup", "snapshot"}, "")
129+
case iso.ListCmd.JSONKeyGetByName:
130+
flagSet.StringSlice("type", []string{"private"}, "")
131+
}
132+
133+
// FlagSet has to be parsed to be populated.
134+
// We pass an empty slice because we defined the flags earlier.
135+
_ = flagSet.Parse([]string{})
136+
137+
result, err := lc.Fetch(ctx, client, flagSet, listOpts, []string{})
138+
ch <- response{result, err}
139+
}()
140+
}
141+
142+
// Wait for all requests to finish and collect results
143+
resources := make([][]any, len(cmds))
144+
for i, responseCh := range responseChs {
145+
response := <-responseCh
146+
if err := response.err; err != nil {
147+
return err
148+
}
149+
resources[i] = response.result
150+
}
151+
152+
if outOpts.IsSet("json") {
153+
jsonSchema := make(map[string]any)
154+
for i, lc := range cmds {
155+
jsonSchema[lc.JSONKeyGetByName] = lc.JSONSchema(resources[i])
156+
}
157+
jsonBytes, err := json.Marshal(jsonSchema)
158+
if err != nil {
159+
return err
160+
}
161+
fmt.Printf("%s\n", jsonBytes)
162+
return nil
163+
}
164+
165+
for i, lc := range cmds {
166+
cols := lc.DefaultColumns
167+
table := lc.OutputTable(client)
168+
table.WriteHeader(cols)
169+
170+
if len(resources[i]) == 0 {
171+
continue
172+
}
173+
174+
fmt.Print(strings.ToUpper(lc.ResourceNamePlural) + "\n---\n")
175+
for _, resource := range resources[i] {
176+
table.Write(cols, resource)
177+
}
178+
if err := table.Flush(); err != nil {
179+
return err
180+
}
181+
fmt.Println()
182+
}
183+
184+
return nil
185+
},
186+
}

0 commit comments

Comments
 (0)