Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

Merged
brettkolodny merged 36 commits intomainfrombrett/coder-sharing-share-command-859
Sep 4, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
36 commits
Select commitHold shift + click to select a range
ea0a84b
feat: add command for sharing a workspace with another user
brettkolodnyAug 22, 2025
192d66f
feat: add sharing command group
brettkolodnyAug 22, 2025
875c358
feat: allow specifying the owner the workspace being shared
brettkolodnyAug 22, 2025
3eb707c
feat: allow sharing workspaces with groups via the share command
brettkolodnyAug 22, 2025
a4e8c14
chore: use namedWorkspace function
brettkolodnyAug 22, 2025
27e70ce
chore: improve error messaging and add descriptions
brettkolodnyAug 22, 2025
26b8d99
chore: setup initial tests for sharing share command
brettkolodnyAug 22, 2025
ed165f1
chore: setup initial tests
brettkolodnyAug 25, 2025
8b443b0
Merge branch 'main' into brett/coder-sharing-share-command-859
brettkolodnyAug 25, 2025
d27d3d4
feat: output the ACL state after the share command completes
brettkolodnyAug 25, 2025
3509710
fix: update role column name
brettkolodnyAug 25, 2025
fb5b947
wip
brettkolodnyAug 25, 2025
bbc4d6a
fix: regex should handle valid usernames
brettkolodnyAug 25, 2025
a8565fc
fix: use org members api to get users
brettkolodnyAug 26, 2025
27f9406
chore: add tests for multi-user sharing
brettkolodnyAug 26, 2025
0d061fe
chore: add tests for setting user roles
brettkolodnyAug 26, 2025
2264c80
fix: hide sharing command
brettkolodnyAug 26, 2025
2b5d7d8
chore: add test for adding single group
brettkolodnyAug 26, 2025
ce8aa66
chore: add test for adding multiple groups
brettkolodnyAug 26, 2025
701c9ad
chore: improve code readability
brettkolodnyAug 27, 2025
d8374da
chore: add test for specifying a role on groups
brettkolodnyAug 27, 2025
0963ca6
chore: remove unused code
brettkolodnyAug 27, 2025
c5a0ce1
fix: ignore users and groups with `WorkspaceRoleDeleted`
brettkolodnyAug 27, 2025
f2b7589
fix: remove print
brettkolodnyAug 27, 2025
488e2d1
fix: lint
brettkolodnyAug 27, 2025
c0cad29
fix: test
brettkolodnyAug 27, 2025
020f641
fix: require at least one arg
brettkolodnyAug 27, 2025
b37c8a3
fix: handle passing in no users or groups
brettkolodnyAug 27, 2025
c513f10
chore: improve error message consistency
brettkolodnyAug 27, 2025
9e476f2
fix: require only one arg
brettkolodnyAug 27, 2025
4aac08d
chore: improve stringToWorkspaceRole function
brettkolodnyAug 27, 2025
2fd5889
chore: improve variable name
brettkolodnyAug 27, 2025
a88364c
chore: remove unused import
brettkolodnyAug 27, 2025
e1dfb02
chore: change command from share to add
brettkolodnyAug 28, 2025
5ade177
fix: check for returned nil
brettkolodnySep 4, 2025
96706e3
chore: move default role logic
brettkolodnySep 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletionscli/root.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,6 +99,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
r.portForward(),
r.publickey(),
r.resetPassword(),
r.sharing(),
r.state(),
r.templates(),
r.tokens(),
Expand Down
231 changes: 231 additions & 0 deletionscli/sharing.go
View file
Open in desktop
Original file line numberDiff line numberDiff 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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

In the event that this isnil can we add a check to prevent a panic?

Suggested change
userAndRole:=nameRoleRegex.FindStringSubmatch(user)
userAndRole:=nameRoleRegex.FindStringSubmatch(user)
ifuserAndRole==nil {
returnxerrors.Errorf("invalid user format %q: must match pattern 'username:role'",user)
}

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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Similar validation question touserAndRole here

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)
}
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp