- Notifications
You must be signed in to change notification settings - Fork1.1k
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
Changes fromall commits
ea0a84b192d66f875c3583eb707ca4e8c1427e70ce26b8d99ed165f18b443b0d27d3d43509710fb5b947bbc4d6aa8565fc27f94060d061fe2264c802b5d7d8ce8aa66701c9add8374da0963ca6c5a0ce1f2b7589488e2d1c0cad29020f641b37c8a3c513f109e476f24aac08d2fd5889a88364ce1dfb025ade17796706e3File 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) | ||||||||||||
Contributor 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) | ||||||||||||
Contributor 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.