@@ -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,119 @@ 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
+ 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" )
35
46
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
+ }
49
49
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
+ }
54
57
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 )
58
69
}
70
+ }
59
71
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
+ }
92
76
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 )
96
94
}
97
- cmd . Flags (). StringVar ( & group , "group" , "user" , "the grouping parameter (user|org)" )
95
+ }
98
96
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 }
100
139
}
101
140
102
141
// groupable specifies a structure capable of being an aggregation group of environments (user, org, all)
@@ -135,20 +174,25 @@ func (o orgGrouping) header() string {
135
174
return fmt .Sprintf ("%s\t (%v member%s)" , truncate (o .org .Name , 20 , "..." ), len (o .org .Members ), plural )
136
175
}
137
176
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 {
139
178
tabwriter := tabwriter .NewWriter (writer , 0 , 0 , 4 , ' ' , 0 )
140
179
defer func () { _ = tabwriter .Flush () }()
141
180
142
181
var userResources []aggregatedResources
143
182
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
+ },
147
189
)
148
190
}
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
+ }
152
196
153
197
for _ , u := range userResources {
154
198
_ , _ = fmt .Fprintf (tabwriter , "%s\t %s" , u .header (), u .resources )
@@ -163,6 +207,40 @@ func printResourceTop(writer io.Writer, groups []groupable, labeler envLabeler)
163
207
}
164
208
_ , _ = fmt .Fprint (tabwriter , "\n " )
165
209
}
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
166
244
}
167
245
168
246
type aggregatedResources struct {
0 commit comments