@@ -26,7 +26,8 @@ func (r *RootCmd) organizationRoles(orgContext *OrganizationContext) *serpent.Co
26
26
},
27
27
Children : []* serpent.Command {
28
28
r .showOrganizationRoles (orgContext ),
29
- r .editOrganizationRole (orgContext ),
29
+ r .updateOrganizationRole (orgContext ),
30
+ r .createOrganizationRole (orgContext ),
30
31
},
31
32
}
32
33
return cmd
@@ -99,7 +100,7 @@ func (r *RootCmd) showOrganizationRoles(orgContext *OrganizationContext) *serpen
99
100
return cmd
100
101
}
101
102
102
- func (r * RootCmd ) editOrganizationRole (orgContext * OrganizationContext ) * serpent.Command {
103
+ func (r * RootCmd ) createOrganizationRole (orgContext * OrganizationContext ) * serpent.Command {
103
104
formatter := cliui .NewOutputFormatter (
104
105
cliui .ChangeFormatterData (
105
106
cliui .TableFormat ([]roleTableRow {}, []string {"name" , "display name" , "site permissions" , "organization permissions" , "user permissions" }),
@@ -118,12 +119,12 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
118
119
119
120
client := new (codersdk.Client )
120
121
cmd := & serpent.Command {
121
- Use : "edit <role_name>" ,
122
- Short : "Edit an organization custom role" ,
122
+ Use : "create <role_name>" ,
123
+ Short : "Create a new organization custom role" ,
123
124
Long : FormatExamples (
124
125
Example {
125
126
Description : "Run with an input.json file" ,
126
- Command : "coder roles edit --stdin < role.json" ,
127
+ Command : "coder organization -O <organization_name> roles create --stidin < role.json" ,
127
128
},
128
129
),
129
130
Options : []serpent.Option {
@@ -152,10 +153,13 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
152
153
return err
153
154
}
154
155
155
- createNewRole := true
156
+ existingRoles , err := client .ListOrganizationRoles (ctx , org .ID )
157
+ if err != nil {
158
+ return xerrors .Errorf ("listing existing roles: %w" , err )
159
+ }
160
+
156
161
var customRole codersdk.Role
157
162
if jsonInput {
158
- // JSON Upload mode
159
163
bytes , err := io .ReadAll (inv .Stdin )
160
164
if err != nil {
161
165
return xerrors .Errorf ("reading stdin: %w" , err )
@@ -175,29 +179,148 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
175
179
return xerrors .Errorf ("json input does not appear to be a valid role" )
176
180
}
177
181
178
- existingRoles , err := client .ListOrganizationRoles (ctx , org .ID )
182
+ if role := existingRole (customRole .Name , existingRoles ); role != nil {
183
+ return xerrors .Errorf ("The role %s already exists. If you'd like to edit this role use the update command instead" , customRole .Name )
184
+ }
185
+ } else {
186
+ if len (inv .Args ) == 0 {
187
+ return xerrors .Errorf ("missing role name argument, usage: \" coder organizations roles create <role_name>\" " )
188
+ }
189
+
190
+ if role := existingRole (inv .Args [0 ], existingRoles ); role != nil {
191
+ return xerrors .Errorf ("The role %s already exists. If you'd like to edit this role use the update command instead" , inv .Args [0 ])
192
+ }
193
+
194
+ interactiveRole , err := interactiveOrgRoleEdit (inv , org .ID , nil )
195
+ if err != nil {
196
+ return xerrors .Errorf ("editing role: %w" , err )
197
+ }
198
+
199
+ customRole = * interactiveRole
200
+ }
201
+
202
+ var updated codersdk.Role
203
+ if dryRun {
204
+ // Do not actually post
205
+ updated = customRole
206
+ } else {
207
+ updated , err = client .CreateOrganizationRole (ctx , customRole )
208
+ if err != nil {
209
+ return xerrors .Errorf ("patch role: %w" , err )
210
+ }
211
+ }
212
+
213
+ output , err := formatter .Format (ctx , updated )
214
+ if err != nil {
215
+ return xerrors .Errorf ("formatting: %w" , err )
216
+ }
217
+
218
+ _ , err = fmt .Fprintln (inv .Stdout , output )
219
+ return err
220
+ },
221
+ }
222
+
223
+ return cmd
224
+ }
225
+
226
+ func (r * RootCmd ) updateOrganizationRole (orgContext * OrganizationContext ) * serpent.Command {
227
+ formatter := cliui .NewOutputFormatter (
228
+ cliui .ChangeFormatterData (
229
+ cliui .TableFormat ([]roleTableRow {}, []string {"name" , "display name" , "site permissions" , "organization permissions" , "user permissions" }),
230
+ func (data any ) (any , error ) {
231
+ typed , _ := data .(codersdk.Role )
232
+ return []roleTableRow {roleToTableView (typed )}, nil
233
+ },
234
+ ),
235
+ cliui .JSONFormat (),
236
+ )
237
+
238
+ var (
239
+ dryRun bool
240
+ jsonInput bool
241
+ )
242
+
243
+ client := new (codersdk.Client )
244
+ cmd := & serpent.Command {
245
+ Use : "update <role_name>" ,
246
+ Short : "Update an organization custom role" ,
247
+ Long : FormatExamples (
248
+ Example {
249
+ Description : "Run with an input.json file" ,
250
+ Command : "coder roles update --stdin < role.json" ,
251
+ },
252
+ ),
253
+ Options : []serpent.Option {
254
+ cliui .SkipPromptOption (),
255
+ {
256
+ Name : "dry-run" ,
257
+ Description : "Does all the work, but does not submit the final updated role." ,
258
+ Flag : "dry-run" ,
259
+ Value : serpent .BoolOf (& dryRun ),
260
+ },
261
+ {
262
+ Name : "stdin" ,
263
+ Description : "Reads stdin for the json role definition to upload." ,
264
+ Flag : "stdin" ,
265
+ Value : serpent .BoolOf (& jsonInput ),
266
+ },
267
+ },
268
+ Middleware : serpent .Chain (
269
+ serpent .RequireRangeArgs (0 , 1 ),
270
+ r .InitClient (client ),
271
+ ),
272
+ Handler : func (inv * serpent.Invocation ) error {
273
+ ctx := inv .Context ()
274
+ org , err := orgContext .Selected (inv , client )
275
+ if err != nil {
276
+ return err
277
+ }
278
+
279
+ existingRoles , err := client .ListOrganizationRoles (ctx , org .ID )
280
+ if err != nil {
281
+ return xerrors .Errorf ("listing existing roles: %w" , err )
282
+ }
283
+
284
+ var customRole codersdk.Role
285
+ if jsonInput {
286
+ bytes , err := io .ReadAll (inv .Stdin )
287
+ if err != nil {
288
+ return xerrors .Errorf ("reading stdin: %w" , err )
289
+ }
290
+
291
+ err = json .Unmarshal (bytes , & customRole )
179
292
if err != nil {
180
- return xerrors .Errorf ("listing existing roles : %w" , err )
293
+ return xerrors .Errorf ("parsing stdin json : %w" , err )
181
294
}
182
- for _ , existingRole := range existingRoles {
183
- if strings .EqualFold (customRole .Name , existingRole .Name ) {
184
- // Editing an existing role
185
- createNewRole = false
186
- break
295
+
296
+ if customRole .Name == "" {
297
+ arr := make ([]json.RawMessage , 0 )
298
+ err = json .Unmarshal (bytes , & arr )
299
+ if err == nil && len (arr ) > 0 {
300
+ return xerrors .Errorf ("only 1 role can be sent at a time" )
187
301
}
302
+ return xerrors .Errorf ("json input does not appear to be a valid role" )
303
+ }
304
+
305
+ if role := existingRole (customRole .Name , existingRoles ); role == nil {
306
+ return xerrors .Errorf ("The role %s does not exist. If you'd like to create this role use the create command instead" , customRole .Name )
188
307
}
189
308
} else {
190
309
if len (inv .Args ) == 0 {
191
310
return xerrors .Errorf ("missing role name argument, usage: \" coder organizations roles edit <role_name>\" " )
192
311
}
193
312
194
- interactiveRole , newRole , err := interactiveOrgRoleEdit (inv , org .ID , client )
313
+ role := existingRole (inv .Args [0 ], existingRoles )
314
+ if role == nil {
315
+ return xerrors .Errorf ("The role %s does not exist. If you'd like to create this role use the create command instead" , inv .Args [0 ])
316
+ }
317
+
318
+ interactiveRole , err := interactiveOrgRoleEdit (inv , org .ID , & role .Role )
195
319
if err != nil {
196
320
return xerrors .Errorf ("editing role: %w" , err )
197
321
}
198
322
199
323
customRole = * interactiveRole
200
- createNewRole = newRole
201
324
202
325
preview := fmt .Sprintf ("permissions: %d site, %d org, %d user" ,
203
326
len (customRole .SitePermissions ), len (customRole .OrganizationPermissions ), len (customRole .UserPermissions ))
@@ -216,12 +339,7 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
216
339
// Do not actually post
217
340
updated = customRole
218
341
} else {
219
- switch createNewRole {
220
- case true :
221
- updated , err = client .CreateOrganizationRole (ctx , customRole )
222
- default :
223
- updated , err = client .UpdateOrganizationRole (ctx , customRole )
224
- }
342
+ updated , err = client .UpdateOrganizationRole (ctx , customRole )
225
343
if err != nil {
226
344
return xerrors .Errorf ("patch role: %w" , err )
227
345
}
@@ -241,50 +359,27 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
241
359
return cmd
242
360
}
243
361
244
- func interactiveOrgRoleEdit (inv * serpent.Invocation , orgID uuid.UUID , client * codersdk.Client ) (* codersdk.Role , bool , error ) {
245
- newRole := false
246
- ctx := inv .Context ()
247
- roles , err := client .ListOrganizationRoles (ctx , orgID )
248
- if err != nil {
249
- return nil , newRole , xerrors .Errorf ("listing roles: %w" , err )
250
- }
251
-
252
- // Make sure the role actually exists first
253
- var originalRole codersdk.AssignableRoles
254
- for _ , r := range roles {
255
- if strings .EqualFold (inv .Args [0 ], r .Name ) {
256
- originalRole = r
257
- break
258
- }
259
- }
260
-
261
- if originalRole .Name == "" {
262
- _ , err = cliui .Prompt (inv , cliui.PromptOptions {
263
- Text : "No organization role exists with that name, do you want to create one?" ,
264
- Default : "yes" ,
265
- IsConfirm : true ,
266
- })
267
- if err != nil {
268
- return nil , newRole , xerrors .Errorf ("abort: %w" , err )
269
- }
270
-
271
- originalRole .Role = codersdk.Role {
362
+ func interactiveOrgRoleEdit (inv * serpent.Invocation , orgID uuid.UUID , updateRole * codersdk.Role ) (* codersdk.Role , error ) {
363
+ var originalRole codersdk.Role
364
+ if updateRole == nil {
365
+ originalRole = codersdk.Role {
272
366
Name : inv .Args [0 ],
273
367
OrganizationID : orgID .String (),
274
368
}
275
- newRole = true
369
+ } else {
370
+ originalRole = * updateRole
276
371
}
277
372
278
373
// Some checks since interactive mode is limited in what it currently sees
279
374
if len (originalRole .SitePermissions ) > 0 {
280
- return nil , newRole , xerrors .Errorf ("unable to edit role in interactive mode, it contains site wide permissions" )
375
+ return nil , xerrors .Errorf ("unable to edit role in interactive mode, it contains site wide permissions" )
281
376
}
282
377
283
378
if len (originalRole .UserPermissions ) > 0 {
284
- return nil , newRole , xerrors .Errorf ("unable to edit role in interactive mode, it contains user permissions" )
379
+ return nil , xerrors .Errorf ("unable to edit role in interactive mode, it contains user permissions" )
285
380
}
286
381
287
- role := & originalRole . Role
382
+ role := & originalRole
288
383
allowedResources := []codersdk.RBACResource {
289
384
codersdk .ResourceTemplate ,
290
385
codersdk .ResourceWorkspace ,
@@ -303,13 +398,13 @@ customRoleLoop:
303
398
Options : append (permissionPreviews (role , allowedResources ), done , abort ),
304
399
})
305
400
if err != nil {
306
- return role , newRole , xerrors .Errorf ("selecting resource: %w" , err )
401
+ return role , xerrors .Errorf ("selecting resource: %w" , err )
307
402
}
308
403
switch selected {
309
404
case done :
310
405
break customRoleLoop
311
406
case abort :
312
- return role , newRole , xerrors .Errorf ("edit role %q aborted" , role .Name )
407
+ return role , xerrors .Errorf ("edit role %q aborted" , role .Name )
313
408
default :
314
409
strs := strings .Split (selected , "::" )
315
410
resource := strings .TrimSpace (strs [0 ])
@@ -320,7 +415,7 @@ customRoleLoop:
320
415
Defaults : defaultActions (role , resource ),
321
416
})
322
417
if err != nil {
323
- return role , newRole , xerrors .Errorf ("selecting actions for resource %q: %w" , resource , err )
418
+ return role , xerrors .Errorf ("selecting actions for resource %q: %w" , resource , err )
324
419
}
325
420
applyOrgResourceActions (role , resource , actions )
326
421
// back to resources!
@@ -329,7 +424,7 @@ customRoleLoop:
329
424
// This println is required because the prompt ends us on the same line as some text.
330
425
_ , _ = fmt .Println ()
331
426
332
- return role , newRole , nil
427
+ return role , nil
333
428
}
334
429
335
430
func applyOrgResourceActions (role * codersdk.Role , resource string , actions []string ) {
@@ -405,6 +500,16 @@ func roleToTableView(role codersdk.Role) roleTableRow {
405
500
}
406
501
}
407
502
503
+ func existingRole (newRoleName string , existingRoles []codersdk.AssignableRoles ) * codersdk.AssignableRoles {
504
+ for _ , existingRole := range existingRoles {
505
+ if strings .EqualFold (newRoleName , existingRole .Name ) {
506
+ return & existingRole
507
+ }
508
+ }
509
+
510
+ return nil
511
+ }
512
+
408
513
type roleTableRow struct {
409
514
Name string `table:"name,default_sort"`
410
515
DisplayName string `table:"display name"`
0 commit comments