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

Commit d11afcd

Browse files
authored
Merge pull request #142 from cdr/resource-filters
Add resource top flags for filtering and sorting
2 parents d9cbba1 + 1185a2f commit d11afcd

File tree

2 files changed

+157
-75
lines changed

2 files changed

+157
-75
lines changed

internal/cmd/cmd.go

Lines changed: 1 addition & 1 deletion
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

Lines changed: 156 additions & 74 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy