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

Lines changed: 163 additions & 58 deletions
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

Lines changed: 3 additions & 2 deletions
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.
Lines changed: 24 additions & 0 deletions
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

Lines changed: 3 additions & 3 deletions
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)
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