@@ -9,10 +9,11 @@ import (
9
9
10
10
"cdr.dev/coder-cli/coder-sdk"
11
11
"github.com/spf13/cobra"
12
+ "go.coder.com/flog"
12
13
"golang.org/x/xerrors"
13
14
)
14
15
15
- func makeResourceCmd () * cobra.Command {
16
+ func resourceCmd () * cobra.Command {
16
17
cmd := & cobra.Command {
17
18
Use : "resources" ,
18
19
Short : "manage Coder resources with platform-level context (users, organizations, environments)" ,
@@ -22,81 +23,124 @@ func makeResourceCmd() *cobra.Command {
22
23
return cmd
23
24
}
24
25
26
+ type resourceTopOptions struct {
27
+ group string
28
+ user string
29
+ org string
30
+ sortBy string
31
+ showEmptyGroups bool
32
+ }
33
+
25
34
func resourceTop () * cobra.Command {
26
- var group string
35
+ var options resourceTopOptions
36
+
27
37
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" )
35
51
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
+ }
49
54
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
+ }
54
62
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 )
58
74
}
75
+ }
59
76
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
+ }
92
86
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 )
96
99
}
97
- cmd . Flags (). StringVar ( & group , "group" , "user" , "the grouping parameter (user|org)" )
100
+ }
98
101
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 }
100
144
}
101
145
102
146
// groupable specifies a structure capable of being an aggregation group of environments (user, org, all)
@@ -135,20 +179,24 @@ func (o orgGrouping) header() string {
135
179
return fmt .Sprintf ("%s\t (%v member%s)" , truncate (o .org .Name , 20 , "..." ), len (o .org .Members ), plural )
136
180
}
137
181
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 {
139
183
tabwriter := tabwriter .NewWriter (writer , 0 , 0 , 4 , ' ' , 0 )
140
184
defer func () { _ = tabwriter .Flush () }()
141
185
142
186
var userResources []aggregatedResources
143
187
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
148
199
}
149
- sort .Slice (userResources , func (i , j int ) bool {
150
- return userResources [i ].cpuAllocation > userResources [j ].cpuAllocation
151
- })
152
200
153
201
for _ , u := range userResources {
154
202
_ , _ = fmt .Fprintf (tabwriter , "%s\t %s" , u .header (), u .resources )
@@ -163,6 +211,40 @@ func printResourceTop(writer io.Writer, groups []groupable, labeler envLabeler)
163
211
}
164
212
_ , _ = fmt .Fprint (tabwriter , "\n " )
165
213
}
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
166
248
}
167
249
168
250
type aggregatedResources struct {
0 commit comments