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

Commit 45e7865

Browse files
committed
Add resource top flags for filtering and sorting
1 parent e5d55d0 commit 45e7865

File tree

2 files changed

+152
-74
lines changed

2 files changed

+152
-74
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
makeEnvsCommand(),
2828
makeSyncCmd(),
2929
makeURLCmd(),
30-
makeResourceCmd(),
30+
resourceCmd(),
3131
completionCmd,
3232
genDocs(app),
3333
)

internal/cmd/resourcemanager.go

+151-73
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,119 @@ 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+
RunE: runResourceTop(&options),
40+
}
41+
cmd.Flags().StringVar(&options.group, "group", "user", "the grouping parameter (user|org)")
42+
cmd.Flags().StringVar(&options.user, "user", "", "filter by a user email")
43+
cmd.Flags().StringVar(&options.org, "org", "", "filter by the name of an organization")
44+
cmd.Flags().StringVar(&options.sortBy, "sort-by", "cpu", "field to sort aggregate groups and environments by (cpu|memory)")
45+
cmd.Flags().BoolVar(&options.showEmptyGroups, "show-empty", false, "show groups with zero active environments")
3546

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-
}
47+
return cmd
48+
}
4949

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

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

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-
}
72+
users, err := client.Users(ctx)
73+
if err != nil {
74+
return xerrors.Errorf("get users: %w", err)
75+
}
9276

93-
printResourceTop(os.Stdout, groups, labeler)
94-
return nil
95-
},
77+
orgs, err := client.Organizations(ctx)
78+
if err != nil {
79+
return xerrors.Errorf("get organizations: %w", err)
80+
}
81+
82+
var groups []groupable
83+
var labeler envLabeler
84+
switch options.group {
85+
case "user":
86+
groups, labeler = aggregateByUser(users, orgs, envs, *options)
87+
case "org":
88+
groups, labeler = aggregateByOrg(users, orgs, envs, *options)
89+
default:
90+
return xerrors.Errorf("unknown --group %q", options.group)
91+
}
92+
93+
return printResourceTop(os.Stdout, groups, labeler, options.showEmptyGroups, options.sortBy)
9694
}
97-
cmd.Flags().StringVar(&group, "group", "user", "the grouping parameter (user|org)")
95+
}
9896

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

102141
// groupable specifies a structure capable of being an aggregation group of environments (user, org, all)
@@ -135,20 +174,25 @@ func (o orgGrouping) header() string {
135174
return fmt.Sprintf("%s\t(%v member%s)", truncate(o.org.Name, 20, "..."), len(o.org.Members), plural)
136175
}
137176

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

142181
var userResources []aggregatedResources
143182
for _, group := range groups {
144-
userResources = append(
145-
userResources,
146-
aggregatedResources{groupable: group, resources: aggregateEnvResources(group.environments())},
183+
if !showEmptyGroups && len(group.environments()) < 1 {
184+
continue
185+
}
186+
userResources = append(userResources, aggregatedResources{
187+
groupable: group, resources: aggregateEnvResources(group.environments()),
188+
},
147189
)
148190
}
149-
sort.Slice(userResources, func(i, j int) bool {
150-
return userResources[i].cpuAllocation > userResources[j].cpuAllocation
151-
})
191+
192+
err := sortAggregatedResources(userResources, sortBy)
193+
if err != nil {
194+
return err
195+
}
152196

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

168246
type aggregatedResources struct {

0 commit comments

Comments
 (0)