- Notifications
You must be signed in to change notification settings - Fork1k
feat: addsharing add
command to the CLI#19576
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
ea0a84b
192d66f
875c358
3eb707c
a4e8c14
27e70ce
26b8d99
ed165f1
8b443b0
d27d3d4
3509710
fb5b947
bbc4d6a
a8565fc
27f9406
0d061fe
2264c80
2b5d7d8
ce8aa66
701c9ad
d8374da
0963ca6
c5a0ce1
f2b7589
488e2d1
c0cad29
020f641
b37c8a3
c513f10
9e476f2
4aac08d
2fd5889
a88364c
e1dfb02
5ade177
96706e3
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,231 @@ | ||||||||||||
package cli | ||||||||||||
import ( | ||||||||||||
"fmt" | ||||||||||||
"regexp" | ||||||||||||
"golang.org/x/xerrors" | ||||||||||||
"github.com/coder/coder/v2/cli/cliui" | ||||||||||||
"github.com/coder/coder/v2/codersdk" | ||||||||||||
"github.com/coder/serpent" | ||||||||||||
) | ||||||||||||
const defaultGroupDisplay = "-" | ||||||||||||
type workspaceShareRow struct { | ||||||||||||
User string `table:"user"` | ||||||||||||
Group string `table:"group,default_sort"` | ||||||||||||
Role codersdk.WorkspaceRole `table:"role"` | ||||||||||||
} | ||||||||||||
func (r *RootCmd) sharing() *serpent.Command { | ||||||||||||
orgContext := NewOrganizationContext() | ||||||||||||
cmd := &serpent.Command{ | ||||||||||||
Use: "sharing [subcommand]", | ||||||||||||
Short: "Commands for managing shared workspaces", | ||||||||||||
Aliases: []string{"share"}, | ||||||||||||
Handler: func(inv *serpent.Invocation) error { | ||||||||||||
return inv.Command.HelpHandler(inv) | ||||||||||||
}, | ||||||||||||
Children: []*serpent.Command{r.shareWorkspace(orgContext)}, | ||||||||||||
Hidden: true, | ||||||||||||
} | ||||||||||||
orgContext.AttachOptions(cmd) | ||||||||||||
return cmd | ||||||||||||
} | ||||||||||||
func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Command { | ||||||||||||
var ( | ||||||||||||
// Username regex taken from codersdk/name.go | ||||||||||||
nameRoleRegex = regexp.MustCompile(`(^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)+(?::([A-Za-z0-9-]+))?`) | ||||||||||||
client = new(codersdk.Client) | ||||||||||||
users []string | ||||||||||||
groups []string | ||||||||||||
formatter = cliui.NewOutputFormatter( | ||||||||||||
cliui.TableFormat( | ||||||||||||
[]workspaceShareRow{}, []string{"User", "Group", "Role"}), | ||||||||||||
cliui.JSONFormat(), | ||||||||||||
) | ||||||||||||
) | ||||||||||||
cmd := &serpent.Command{ | ||||||||||||
Use: "add <workspace> --user <user>:<role> --group <group>:<role>", | ||||||||||||
Aliases: []string{"share"}, | ||||||||||||
Short: "Share a workspace with a user or group.", | ||||||||||||
Options: serpent.OptionSet{ | ||||||||||||
{ | ||||||||||||
Name: "user", | ||||||||||||
Description: "A comma separated list of users to share the workspace with.", | ||||||||||||
Flag: "user", | ||||||||||||
Value: serpent.StringArrayOf(&users), | ||||||||||||
}, { | ||||||||||||
Name: "group", | ||||||||||||
Description: "A comma separated list of groups to share the workspace with.", | ||||||||||||
Flag: "group", | ||||||||||||
Value: serpent.StringArrayOf(&groups), | ||||||||||||
}, | ||||||||||||
}, | ||||||||||||
Middleware: serpent.Chain( | ||||||||||||
r.InitClient(client), | ||||||||||||
serpent.RequireNArgs(1), | ||||||||||||
), | ||||||||||||
Handler: func(inv *serpent.Invocation) error { | ||||||||||||
if len(users) == 0 && len(groups) == 0 { | ||||||||||||
return xerrors.New("at least one user or group must be provided") | ||||||||||||
} | ||||||||||||
workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) | ||||||||||||
if err != nil { | ||||||||||||
return xerrors.Errorf("could not fetch the workspace %s: %w", inv.Args[0], err) | ||||||||||||
} | ||||||||||||
org, err := orgContext.Selected(inv, client) | ||||||||||||
if err != nil { | ||||||||||||
return err | ||||||||||||
} | ||||||||||||
userRoles := make(map[string]codersdk.WorkspaceRole, len(users)) | ||||||||||||
if len(users) > 0 { | ||||||||||||
orgMembers, err := client.OrganizationMembers(inv.Context(), org.ID) | ||||||||||||
if err != nil { | ||||||||||||
return err | ||||||||||||
} | ||||||||||||
for _, user := range users { | ||||||||||||
userAndRole := nameRoleRegex.FindStringSubmatch(user) | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. In the event that this is Suggested change
| ||||||||||||
if userAndRole == nil { | ||||||||||||
return xerrors.Errorf("invalid user format %q: must match pattern 'username:role'", user) | ||||||||||||
} | ||||||||||||
username := userAndRole[1] | ||||||||||||
role := userAndRole[2] | ||||||||||||
if role == "" { | ||||||||||||
role = string(codersdk.WorkspaceRoleUse) | ||||||||||||
} | ||||||||||||
userID := "" | ||||||||||||
for _, member := range orgMembers { | ||||||||||||
if member.Username == username { | ||||||||||||
userID = member.UserID.String() | ||||||||||||
break | ||||||||||||
} | ||||||||||||
} | ||||||||||||
if userID == "" { | ||||||||||||
return xerrors.Errorf("could not find user %s in the organization %s", username, org.Name) | ||||||||||||
} | ||||||||||||
workspaceRole, err := stringToWorkspaceRole(role) | ||||||||||||
if err != nil { | ||||||||||||
return err | ||||||||||||
} | ||||||||||||
userRoles[userID] = workspaceRole | ||||||||||||
} | ||||||||||||
} | ||||||||||||
groupRoles := make(map[string]codersdk.WorkspaceRole) | ||||||||||||
if len(groups) > 0 { | ||||||||||||
orgGroups, err := client.Groups(inv.Context(), codersdk.GroupArguments{ | ||||||||||||
Organization: org.ID.String(), | ||||||||||||
}) | ||||||||||||
if err != nil { | ||||||||||||
return err | ||||||||||||
} | ||||||||||||
for _, group := range groups { | ||||||||||||
groupAndRole := nameRoleRegex.FindStringSubmatch(group) | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Similar validation question to | ||||||||||||
if groupAndRole == nil { | ||||||||||||
return xerrors.Errorf("invalid group format %q: must match pattern 'group:role'", group) | ||||||||||||
} | ||||||||||||
groupName := groupAndRole[1] | ||||||||||||
role := groupAndRole[2] | ||||||||||||
if role == "" { | ||||||||||||
role = string(codersdk.WorkspaceRoleUse) | ||||||||||||
} | ||||||||||||
var orgGroup *codersdk.Group | ||||||||||||
for _, group := range orgGroups { | ||||||||||||
if group.Name == groupName { | ||||||||||||
orgGroup = &group | ||||||||||||
break | ||||||||||||
} | ||||||||||||
} | ||||||||||||
if orgGroup == nil { | ||||||||||||
return xerrors.Errorf("could not find group named %s belonging to the organization %s", groupName, org.Name) | ||||||||||||
} | ||||||||||||
workspaceRole, err := stringToWorkspaceRole(role) | ||||||||||||
if err != nil { | ||||||||||||
return err | ||||||||||||
} | ||||||||||||
groupRoles[orgGroup.ID.String()] = workspaceRole | ||||||||||||
} | ||||||||||||
} | ||||||||||||
err = client.UpdateWorkspaceACL(inv.Context(), workspace.ID, codersdk.UpdateWorkspaceACL{ | ||||||||||||
UserRoles: userRoles, | ||||||||||||
GroupRoles: groupRoles, | ||||||||||||
}) | ||||||||||||
if err != nil { | ||||||||||||
return err | ||||||||||||
} | ||||||||||||
workspaceACL, err := client.WorkspaceACL(inv.Context(), workspace.ID) | ||||||||||||
if err != nil { | ||||||||||||
return xerrors.Errorf("could not fetch current workspace ACL after sharing %w", err) | ||||||||||||
} | ||||||||||||
outputRows := make([]workspaceShareRow, 0) | ||||||||||||
for _, user := range workspaceACL.Users { | ||||||||||||
if user.Role == codersdk.WorkspaceRoleDeleted { | ||||||||||||
continue | ||||||||||||
} | ||||||||||||
outputRows = append(outputRows, workspaceShareRow{ | ||||||||||||
User: user.Username, | ||||||||||||
Group: defaultGroupDisplay, | ||||||||||||
Role: user.Role, | ||||||||||||
}) | ||||||||||||
} | ||||||||||||
for _, group := range workspaceACL.Groups { | ||||||||||||
if group.Role == codersdk.WorkspaceRoleDeleted { | ||||||||||||
continue | ||||||||||||
} | ||||||||||||
for _, user := range group.Members { | ||||||||||||
outputRows = append(outputRows, workspaceShareRow{ | ||||||||||||
User: user.Username, | ||||||||||||
Group: group.Name, | ||||||||||||
Role: group.Role, | ||||||||||||
}) | ||||||||||||
} | ||||||||||||
} | ||||||||||||
out, err := formatter.Format(inv.Context(), outputRows) | ||||||||||||
if err != nil { | ||||||||||||
return err | ||||||||||||
} | ||||||||||||
_, err = fmt.Fprintln(inv.Stdout, out) | ||||||||||||
return err | ||||||||||||
}, | ||||||||||||
} | ||||||||||||
return cmd | ||||||||||||
} | ||||||||||||
func stringToWorkspaceRole(role string) (codersdk.WorkspaceRole, error) { | ||||||||||||
switch role { | ||||||||||||
case string(codersdk.WorkspaceRoleUse): | ||||||||||||
return codersdk.WorkspaceRoleUse, nil | ||||||||||||
case string(codersdk.WorkspaceRoleAdmin): | ||||||||||||
return codersdk.WorkspaceRoleAdmin, nil | ||||||||||||
default: | ||||||||||||
return "", xerrors.Errorf("invalid role %q: expected %q or %q", | ||||||||||||
role, codersdk.WorkspaceRoleAdmin, codersdk.WorkspaceRoleUse) | ||||||||||||
} | ||||||||||||
} |
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.