Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 1185a2f

Browse files
committed
Add resource top flags for filtering and sorting
1 parent d9cbba1 commit 1185a2f

File tree

2 files changed

+157
-75
lines changed

2 files changed

+157
-75
lines changed

internal/cmd/cmd.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func Make() *cobra.Command {
2727
envsCommand(),
2828
makeSyncCmd(),
2929
makeURLCmd(),
30-
makeResourceCmd(),
30+
resourceCmd(),
3131
completionCmd,
3232
genDocs(app),
3333
)

internal/cmd/resourcemanager.go

+156-74
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import (
99

1010
"cdr.dev/coder-cli/coder-sdk"
1111
"github.com/spf13/cobra"
12+
"go.coder.com/flog"
1213
"golang.org/x/xerrors"
1314
)
1415

15-
func makeResourceCmd() *cobra.Command {
16+
func resourceCmd() *cobra.Command {
1617
cmd := &cobra.Command{
1718
Use: "resources",
1819
Short: "manage Coder resources with platform-level context (users, organizations, environments)",
@@ -22,81 +23,124 @@ func makeResourceCmd() *cobra.Command {
2223
return cmd
2324
}
2425

26+
type resourceTopOptions struct {
27+
group string
28+
user string
29+
org string
30+
sortBy string
31+
showEmptyGroups bool
32+
}
33+
2534
func resourceTop() *cobra.Command {
26-
var group string
35+
var options resourceTopOptions
36+
2737
cmd := &cobra.Command{
28-
Use: "top",
29-
RunE: func(cmd *cobra.Command, args []string) error {
30-
ctx := cmd.Context()
31-
client, err := newClient()
32-
if err != nil {
33-
return err
34-
}
38+
Use: "top",
39+
Short: "resource viewer with Coder platform annotations",
40+
RunE: runResourceTop(&options),
41+
Example: `coder resources top --group org
42+
coder resources top --group org --verbose --org DevOps
43+
coder resources top --group user --verbose --user name@example.com
44+
coder resources top --sort-by memory --show-empty`,
45+
}
46+
cmd.Flags().StringVar(&options.group, "group", "user", "the grouping parameter (user|org)")
47+
cmd.Flags().StringVar(&options.user, "user", "", "filter by a user email")
48+
cmd.Flags().StringVar(&options.org, "org", "", "filter by the name of an organization")
49+
cmd.Flags().StringVar(&options.sortBy, "sort-by", "cpu", "field to sort aggregate groups and environments by (cpu|memory)")
50+
cmd.Flags().BoolVar(&options.showEmptyGroups, "show-empty", false, "show groups with zero active environments")
3551

36-
// NOTE: it's not worth parrallelizing these calls yet given that this specific endpoint
37-
// takes about 20x times longer than the other two
38-
allEnvs, err := client.Environments(ctx)
39-
if err != nil {
40-
return xerrors.Errorf("get environments %w", err)
41-
}
42-
// only include environments whose last status was "ON"
43-
envs := make([]coder.Environment, 0)
44-
for _, e := range allEnvs {
45-
if e.LatestStat.ContainerStatus == coder.EnvironmentOn {
46-
envs = append(envs, e)
47-
}
48-
}
52+
return cmd
53+
}
4954

50-
users, err := client.Users(ctx)
51-
if err != nil {
52-
return xerrors.Errorf("get users: %w", err)
53-
}
55+
func runResourceTop(options *resourceTopOptions) func(cmd *cobra.Command, args []string) error {
56+
return func(cmd *cobra.Command, args []string) error {
57+
ctx := cmd.Context()
58+
client, err := newClient()
59+
if err != nil {
60+
return err
61+
}
5462

55-
orgs, err := client.Organizations(ctx)
56-
if err != nil {
57-
return xerrors.Errorf("get organizations: %w", err)
63+
// NOTE: it's not worth parrallelizing these calls yet given that this specific endpoint
64+
// takes about 20x times longer than the other two
65+
allEnvs, err := client.Environments(ctx)
66+
if err != nil {
67+
return xerrors.Errorf("get environments %w", err)
68+
}
69+
// only include environments whose last status was "ON"
70+
envs := make([]coder.Environment, 0)
71+
for _, e := range allEnvs {
72+
if e.LatestStat.ContainerStatus == coder.EnvironmentOn {
73+
envs = append(envs, e)
5874
}
75+
}
5976

60-
var groups []groupable
61-
var labeler envLabeler
62-
switch group {
63-
case "user":
64-
userEnvs := make(map[string][]coder.Environment, len(users))
65-
for _, e := range envs {
66-
userEnvs[e.UserID] = append(userEnvs[e.UserID], e)
67-
}
68-
for _, u := range users {
69-
groups = append(groups, userGrouping{user: u, envs: userEnvs[u.ID]})
70-
}
71-
orgIDMap := make(map[string]coder.Organization)
72-
for _, o := range orgs {
73-
orgIDMap[o.ID] = o
74-
}
75-
labeler = orgLabeler{orgIDMap}
76-
case "org":
77-
orgEnvs := make(map[string][]coder.Environment, len(orgs))
78-
for _, e := range envs {
79-
orgEnvs[e.OrganizationID] = append(orgEnvs[e.OrganizationID], e)
80-
}
81-
for _, o := range orgs {
82-
groups = append(groups, orgGrouping{org: o, envs: orgEnvs[o.ID]})
83-
}
84-
userIDMap := make(map[string]coder.User)
85-
for _, u := range users {
86-
userIDMap[u.ID] = u
87-
}
88-
labeler = userLabeler{userIDMap}
89-
default:
90-
return xerrors.Errorf("unknown --group %q", group)
91-
}
77+
users, err := client.Users(ctx)
78+
if err != nil {
79+
return xerrors.Errorf("get users: %w", err)
80+
}
81+
82+
orgs, err := client.Organizations(ctx)
83+
if err != nil {
84+
return xerrors.Errorf("get organizations: %w", err)
85+
}
9286

93-
printResourceTop(os.Stdout, groups, labeler)
94-
return nil
95-
},
87+
var groups []groupable
88+
var labeler envLabeler
89+
switch options.group {
90+
case "user":
91+
groups, labeler = aggregateByUser(users, orgs, envs, *options)
92+
case "org":
93+
groups, labeler = aggregateByOrg(users, orgs, envs, *options)
94+
default:
95+
return xerrors.Errorf("unknown --group %q", options.group)
96+
}
97+
98+
return printResourceTop(os.Stdout, groups, labeler, options.showEmptyGroups, options.sortBy)
9699
}
97-
cmd.Flags().StringVar(&group, "group", "user", "the grouping parameter (user|org)")
100+
}
98101

99-
return cmd
102+
func aggregateByUser(users []coder.User, orgs []coder.Organization, envs []coder.Environment, options resourceTopOptions) ([]groupable, envLabeler) {
103+
var groups []groupable
104+
orgIDMap := make(map[string]coder.Organization)
105+
for _, o := range orgs {
106+
orgIDMap[o.ID] = o
107+
}
108+
userEnvs := make(map[string][]coder.Environment, len(users))
109+
for _, e := range envs {
110+
if options.org != "" && orgIDMap[e.OrganizationID].Name != options.org {
111+
continue
112+
}
113+
userEnvs[e.UserID] = append(userEnvs[e.UserID], e)
114+
}
115+
for _, u := range users {
116+
if options.user != "" && u.Email != options.user {
117+
continue
118+
}
119+
groups = append(groups, userGrouping{user: u, envs: userEnvs[u.ID]})
120+
}
121+
return groups, orgLabeler{orgIDMap}
122+
}
123+
124+
func aggregateByOrg(users []coder.User, orgs []coder.Organization, envs []coder.Environment, options resourceTopOptions) ([]groupable, envLabeler) {
125+
var groups []groupable
126+
userIDMap := make(map[string]coder.User)
127+
for _, u := range users {
128+
userIDMap[u.ID] = u
129+
}
130+
orgEnvs := make(map[string][]coder.Environment, len(orgs))
131+
for _, e := range envs {
132+
if options.user != "" && userIDMap[e.UserID].Email != options.user {
133+
continue
134+
}
135+
orgEnvs[e.OrganizationID] = append(orgEnvs[e.OrganizationID], e)
136+
}
137+
for _, o := range orgs {
138+
if options.org != "" && o.Name != options.org {
139+
continue
140+
}
141+
groups = append(groups, orgGrouping{org: o, envs: orgEnvs[o.ID]})
142+
}
143+
return groups, userLabeler{userIDMap}
100144
}
101145

102146
// groupable specifies a structure capable of being an aggregation group of environments (user, org, all)
@@ -135,20 +179,24 @@ func (o orgGrouping) header() string {
135179
return fmt.Sprintf("%s\t(%v member%s)", truncate(o.org.Name, 20, "..."), len(o.org.Members), plural)
136180
}
137181

138-
func printResourceTop(writer io.Writer, groups []groupable, labeler envLabeler) {
182+
func printResourceTop(writer io.Writer, groups []groupable, labeler envLabeler, showEmptyGroups bool, sortBy string) error {
139183
tabwriter := tabwriter.NewWriter(writer, 0, 0, 4, ' ', 0)
140184
defer func() { _ = tabwriter.Flush() }()
141185

142186
var userResources []aggregatedResources
143187
for _, group := range groups {
144-
userResources = append(
145-
userResources,
146-
aggregatedResources{groupable: group, resources: aggregateEnvResources(group.environments())},
147-
)
188+
if !showEmptyGroups && len(group.environments()) < 1 {
189+
continue
190+
}
191+
userResources = append(userResources, aggregatedResources{
192+
groupable: group, resources: aggregateEnvResources(group.environments()),
193+
})
194+
}
195+
196+
err := sortAggregatedResources(userResources, sortBy)
197+
if err != nil {
198+
return err
148199
}
149-
sort.Slice(userResources, func(i, j int) bool {
150-
return userResources[i].cpuAllocation > userResources[j].cpuAllocation
151-
})
152200

153201
for _, u := range userResources {
154202
_, _ = fmt.Fprintf(tabwriter, "%s\t%s", u.header(), u.resources)
@@ -163,6 +211,40 @@ func printResourceTop(writer io.Writer, groups []groupable, labeler envLabeler)
163211
}
164212
_, _ = fmt.Fprint(tabwriter, "\n")
165213
}
214+
if len(userResources) == 0 {
215+
flog.Info("No groups for the given filters exist with active environments.")
216+
flog.Info("Use \"--show-empty\" to see groups with no resources.")
217+
}
218+
return nil
219+
}
220+
221+
func sortAggregatedResources(resources []aggregatedResources, sortBy string) error {
222+
const cpu = "cpu"
223+
const memory = "memory"
224+
switch sortBy {
225+
case cpu:
226+
sort.Slice(resources, func(i, j int) bool {
227+
return resources[i].cpuAllocation > resources[j].cpuAllocation
228+
})
229+
case memory:
230+
sort.Slice(resources, func(i, j int) bool {
231+
return resources[i].memAllocation > resources[j].memAllocation
232+
})
233+
default:
234+
return xerrors.Errorf("unknown --sort-by value of \"%s\"", sortBy)
235+
}
236+
for _, group := range resources {
237+
envs := group.environments()
238+
switch sortBy {
239+
case cpu:
240+
sort.Slice(envs, func(i, j int) bool { return envs[i].CPUCores > envs[j].CPUCores })
241+
case memory:
242+
sort.Slice(envs, func(i, j int) bool { return envs[i].MemoryGB > envs[j].MemoryGB })
243+
default:
244+
return xerrors.Errorf("unknown --sort-by value of \"%s\"", sortBy)
245+
}
246+
}
247+
return nil
166248
}
167249

168250
type aggregatedResources struct {

0 commit comments

Comments
 (0)