Skip to content

Commit ae7afd1

Browse files
authored
feat: split cli roles edit command into create and update commands (#17121)
Closes #14239
1 parent 53af7e1 commit ae7afd1

9 files changed

+361
-77
lines changed

cli/organizationroles.go

+163-58
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ func (r *RootCmd) organizationRoles(orgContext *OrganizationContext) *serpent.Co
2626
},
2727
Children: []*serpent.Command{
2828
r.showOrganizationRoles(orgContext),
29-
r.editOrganizationRole(orgContext),
29+
r.updateOrganizationRole(orgContext),
30+
r.createOrganizationRole(orgContext),
3031
},
3132
}
3233
return cmd
@@ -99,7 +100,7 @@ func (r *RootCmd) showOrganizationRoles(orgContext *OrganizationContext) *serpen
99100
return cmd
100101
}
101102

102-
func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent.Command {
103+
func (r *RootCmd) createOrganizationRole(orgContext *OrganizationContext) *serpent.Command {
103104
formatter := cliui.NewOutputFormatter(
104105
cliui.ChangeFormatterData(
105106
cliui.TableFormat([]roleTableRow{}, []string{"name", "display name", "site permissions", "organization permissions", "user permissions"}),
@@ -118,12 +119,12 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
118119

119120
client := new(codersdk.Client)
120121
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",
123124
Long: FormatExamples(
124125
Example{
125126
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",
127128
},
128129
),
129130
Options: []serpent.Option{
@@ -152,10 +153,13 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
152153
return err
153154
}
154155

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+
156161
var customRole codersdk.Role
157162
if jsonInput {
158-
// JSON Upload mode
159163
bytes, err := io.ReadAll(inv.Stdin)
160164
if err != nil {
161165
return xerrors.Errorf("reading stdin: %w", err)
@@ -175,29 +179,148 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
175179
return xerrors.Errorf("json input does not appear to be a valid role")
176180
}
177181

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)
179292
if err != nil {
180-
return xerrors.Errorf("listing existing roles: %w", err)
293+
return xerrors.Errorf("parsing stdin json: %w", err)
181294
}
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")
187301
}
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)
188307
}
189308
} else {
190309
if len(inv.Args) == 0 {
191310
return xerrors.Errorf("missing role name argument, usage: \"coder organizations roles edit <role_name>\"")
192311
}
193312

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)
195319
if err != nil {
196320
return xerrors.Errorf("editing role: %w", err)
197321
}
198322

199323
customRole = *interactiveRole
200-
createNewRole = newRole
201324

202325
preview := fmt.Sprintf("permissions: %d site, %d org, %d user",
203326
len(customRole.SitePermissions), len(customRole.OrganizationPermissions), len(customRole.UserPermissions))
@@ -216,12 +339,7 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
216339
// Do not actually post
217340
updated = customRole
218341
} 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)
225343
if err != nil {
226344
return xerrors.Errorf("patch role: %w", err)
227345
}
@@ -241,50 +359,27 @@ func (r *RootCmd) editOrganizationRole(orgContext *OrganizationContext) *serpent
241359
return cmd
242360
}
243361

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{
272366
Name: inv.Args[0],
273367
OrganizationID: orgID.String(),
274368
}
275-
newRole = true
369+
} else {
370+
originalRole = *updateRole
276371
}
277372

278373
// Some checks since interactive mode is limited in what it currently sees
279374
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")
281376
}
282377

283378
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")
285380
}
286381

287-
role := &originalRole.Role
382+
role := &originalRole
288383
allowedResources := []codersdk.RBACResource{
289384
codersdk.ResourceTemplate,
290385
codersdk.ResourceWorkspace,
@@ -303,13 +398,13 @@ customRoleLoop:
303398
Options: append(permissionPreviews(role, allowedResources), done, abort),
304399
})
305400
if err != nil {
306-
return role, newRole, xerrors.Errorf("selecting resource: %w", err)
401+
return role, xerrors.Errorf("selecting resource: %w", err)
307402
}
308403
switch selected {
309404
case done:
310405
break customRoleLoop
311406
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)
313408
default:
314409
strs := strings.Split(selected, "::")
315410
resource := strings.TrimSpace(strs[0])
@@ -320,7 +415,7 @@ customRoleLoop:
320415
Defaults: defaultActions(role, resource),
321416
})
322417
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)
324419
}
325420
applyOrgResourceActions(role, resource, actions)
326421
// back to resources!
@@ -329,7 +424,7 @@ customRoleLoop:
329424
// This println is required because the prompt ends us on the same line as some text.
330425
_, _ = fmt.Println()
331426

332-
return role, newRole, nil
427+
return role, nil
333428
}
334429

335430
func applyOrgResourceActions(role *codersdk.Role, resource string, actions []string) {
@@ -405,6 +500,16 @@ func roleToTableView(role codersdk.Role) roleTableRow {
405500
}
406501
}
407502

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+
408513
type roleTableRow struct {
409514
Name string `table:"name,default_sort"`
410515
DisplayName string `table:"display name"`

cli/testdata/coder_organizations_roles_--help.golden

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ USAGE:
88
Aliases: role
99

1010
SUBCOMMANDS:
11-
edit Edit an organization custom role
12-
show Show role(s)
11+
create Create a new organization custom role
12+
show Show role(s)
13+
update Update an organization custom role
1314

1415
———
1516
Run `coder --help` for a list of global options.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
coder v0.0.0-devel
2+
3+
USAGE:
4+
coder organizations roles create [flags] <role_name>
5+
6+
Create a new organization custom role
7+
8+
- Run with an input.json file:
9+
10+
$ coder organization -O <organization_name> roles create --stidin <
11+
role.json
12+
13+
OPTIONS:
14+
--dry-run bool
15+
Does all the work, but does not submit the final updated role.
16+
17+
--stdin bool
18+
Reads stdin for the json role definition to upload.
19+
20+
-y, --yes bool
21+
Bypass prompts.
22+
23+
———
24+
Run `coder --help` for a list of global options.

cli/testdata/coder_organizations_roles_edit_--help.golden renamed to cli/testdata/coder_organizations_roles_update_--help.golden

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
coder v0.0.0-devel
22

33
USAGE:
4-
coder organizations roles edit [flags] <role_name>
4+
coder organizations roles update [flags] <role_name>
55

6-
Edit an organization custom role
6+
Update an organization custom role
77

88
- Run with an input.json file:
99

10-
$ coder roles edit --stdin < role.json
10+
$ coder roles update --stdin < role.json
1111

1212
OPTIONS:
1313
-c, --column [name|display name|organization id|site permissions|organization permissions|user permissions] (default: name,display name,site permissions,organization permissions,user permissions)

0 commit comments

Comments
 (0)