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

Commitd0fc81a

Browse files
authored
chore: implement cli list organization members (#13555)
example cli command: `coder organization members`
1 parentbbe23ed commitd0fc81a

File tree

10 files changed

+317
-45
lines changed

10 files changed

+317
-45
lines changed

‎cli/cliui/table.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,24 @@ func renderTable(out any, sort string, headers table.Row, filterColumns []string
205205
}
206206
}
207207

208+
// Guard against nil dereferences
209+
ifv!=nil {
210+
rt:=reflect.TypeOf(v)
211+
switchrt.Kind() {
212+
casereflect.Slice:
213+
// By default, the behavior is '%v', which just returns a string like
214+
// '[a b c]'. This will add commas in between each value.
215+
strs:=make([]string,0)
216+
vt:=reflect.ValueOf(v)
217+
fori:=0;i<vt.Len();i++ {
218+
strs=append(strs,fmt.Sprintf("%v",vt.Index(i).Interface()))
219+
}
220+
v="["+strings.Join(strs,", ")+"]"
221+
default:
222+
// Leave it as it is
223+
}
224+
}
225+
208226
rowSlice[i]=v
209227
}
210228

‎cli/cliui/table_test.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,10 @@ func Test_DisplayTable(t *testing.T) {
138138
t.Parallel()
139139

140140
expected:=`
141-
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
142-
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
143-
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
144-
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
141+
NAME AGE ROLESSUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
142+
bar 20 [a]bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
143+
baz 30 []baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
144+
foo 10 [a, b, c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
145145
`
146146

147147
// Test with non-pointer values.
@@ -165,10 +165,10 @@ foo 10 [a b c] foo1 11 foo2 12 foo3
165165
t.Parallel()
166166

167167
expected:=`
168-
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
169-
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
170-
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
171-
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
168+
NAME AGE ROLESSUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
169+
foo 10 [a, b, c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
170+
bar 20 [a]bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
171+
baz 30 []baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
172172
`
173173

174174
out,err:=cliui.DisplayTable(in,"age",nil)
@@ -235,12 +235,12 @@ Alice 25
235235
t.Run("WithSeparator",func(t*testing.T) {
236236
t.Parallel()
237237
expected:=`
238-
NAME AGE ROLES SUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
239-
bar 20 [a] bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
240-
-------------------------------------------------------------------------------------------------------------------------------------------------------------
241-
baz 30 [] baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
242-
-------------------------------------------------------------------------------------------------------------------------------------------------------------
243-
foo 10 [a b c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
238+
NAME AGE ROLESSUB 1 NAME SUB 1 AGE SUB 2 NAME SUB 2 AGE SUB 3 INNER NAME SUB 3 INNER AGE SUB 4 TIME TIME PTR
239+
bar 20 [a]bar1 21 <nil> <nil> bar3 23 {bar4 24 } 2022-08-02T15:49:10Z <nil>
240+
---------------------------------------------------------------------------------------------------------------------------------------------------------------
241+
baz 30 []baz1 31 <nil> <nil> baz3 33 {baz4 34 } 2022-08-02T15:49:10Z <nil>
242+
---------------------------------------------------------------------------------------------------------------------------------------------------------------
243+
foo 10 [a, b, c] foo1 11 foo2 12 foo3 13 {foo4 14 } 2022-08-02T15:49:10Z 2022-08-02T15:49:10Z
244244
`
245245

246246
varinlineIn []any

‎cli/organization.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@ import (
1818

1919
func (r*RootCmd)organizations()*serpent.Command {
2020
cmd:=&serpent.Command{
21-
Annotations:workspaceCommand,
22-
Use:"organizations [subcommand]",
23-
Short:"Organization related commands",
24-
Aliases: []string{"organization","org","orgs"},
25-
Hidden:true,// Hidden until these commands are complete.
21+
Use:"organizations [subcommand]",
22+
Short:"Organization related commands",
23+
Aliases: []string{"organization","org","orgs"},
24+
Hidden:true,// Hidden until these commands are complete.
2625
Handler:func(inv*serpent.Invocation)error {
2726
returninv.Command.HelpHandler(inv)
2827
},
@@ -31,6 +30,7 @@ func (r *RootCmd) organizations() *serpent.Command {
3130
r.switchOrganization(),
3231
r.createOrganization(),
3332
r.organizationRoles(),
33+
r.organizationMembers(),
3434
},
3535
}
3636

‎cli/organizationmembers.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
6+
"golang.org/x/xerrors"
7+
8+
"github.com/coder/coder/v2/cli/cliui"
9+
"github.com/coder/coder/v2/codersdk"
10+
"github.com/coder/serpent"
11+
)
12+
13+
func (r*RootCmd)organizationMembers()*serpent.Command {
14+
formatter:=cliui.NewOutputFormatter(
15+
cliui.TableFormat([]codersdk.OrganizationMemberWithName{}, []string{"username","organization_roles"}),
16+
cliui.JSONFormat(),
17+
)
18+
19+
client:=new(codersdk.Client)
20+
cmd:=&serpent.Command{
21+
Use:"members",
22+
Short:"List all organization members",
23+
Aliases: []string{"member"},
24+
Middleware:serpent.Chain(
25+
serpent.RequireNArgs(0),
26+
r.InitClient(client),
27+
),
28+
Handler:func(inv*serpent.Invocation)error {
29+
ctx:=inv.Context()
30+
organization,err:=CurrentOrganization(r,inv,client)
31+
iferr!=nil {
32+
returnerr
33+
}
34+
35+
res,err:=client.OrganizationMembers(ctx,organization.ID)
36+
iferr!=nil {
37+
returnxerrors.Errorf("fetch members: %w",err)
38+
}
39+
40+
out,err:=formatter.Format(inv.Context(),res)
41+
iferr!=nil {
42+
returnerr
43+
}
44+
45+
_,err=fmt.Fprintln(inv.Stdout,out)
46+
returnerr
47+
},
48+
}
49+
formatter.AttachOptions(&cmd.Options)
50+
51+
returncmd
52+
}

‎cli/organizationmembers_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package cli_test
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/coder/coder/v2/cli/clitest"
10+
"github.com/coder/coder/v2/coderd/coderdtest"
11+
"github.com/coder/coder/v2/coderd/rbac"
12+
"github.com/coder/coder/v2/testutil"
13+
)
14+
15+
funcTestListOrganizationMembers(t*testing.T) {
16+
t.Parallel()
17+
18+
t.Run("OK",func(t*testing.T) {
19+
t.Parallel()
20+
21+
ownerClient:=coderdtest.New(t,&coderdtest.Options{})
22+
owner:=coderdtest.CreateFirstUser(t,ownerClient)
23+
client,user:=coderdtest.CreateAnotherUser(t,ownerClient,owner.OrganizationID,rbac.RoleUserAdmin())
24+
25+
ctx:=testutil.Context(t,testutil.WaitMedium)
26+
inv,root:=clitest.New(t,"organization","members","-c","user_id,username,roles")
27+
clitest.SetupConfig(t,client,root)
28+
29+
buf:=new(bytes.Buffer)
30+
inv.Stdout=buf
31+
err:=inv.WithContext(ctx).Run()
32+
require.NoError(t,err)
33+
require.Contains(t,buf.String(),user.Username)
34+
require.Contains(t,buf.String(),owner.UserID.String())
35+
})
36+
}

‎coderd/members.go

Lines changed: 99 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package coderd
22

33
import (
4+
"context"
45
"net/http"
56

67
"github.com/google/uuid"
7-
8-
"github.com/coder/coder/v2/coderd/database/db2sdk"
9-
"github.com/coder/coder/v2/coderd/rbac"
8+
"golang.org/x/xerrors"
109

1110
"github.com/coder/coder/v2/coderd/database"
11+
"github.com/coder/coder/v2/coderd/database/db2sdk"
1212
"github.com/coder/coder/v2/coderd/httpapi"
1313
"github.com/coder/coder/v2/coderd/httpmw"
14+
"github.com/coder/coder/v2/coderd/rbac"
1415
"github.com/coder/coder/v2/codersdk"
1516
)
1617

@@ -41,7 +42,13 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) {
4142
return
4243
}
4344

44-
httpapi.Write(ctx,rw,http.StatusOK,db2sdk.List(members,convertOrganizationMemberRow))
45+
resp,err:=convertOrganizationMemberRows(ctx,api.Database,members)
46+
iferr!=nil {
47+
httpapi.InternalServerError(rw,err)
48+
return
49+
}
50+
51+
httpapi.Write(ctx,rw,http.StatusOK,resp)
4552
}
4653

4754
// @Summary Assign role to organization member
@@ -87,30 +94,101 @@ func (api *API) putMemberRoles(rw http.ResponseWriter, r *http.Request) {
8794
return
8895
}
8996

90-
httpapi.Write(ctx,rw,http.StatusOK,convertOrganizationMember(updatedUser))
97+
resp,err:=convertOrganizationMembers(ctx,api.Database, []database.OrganizationMember{updatedUser})
98+
iferr!=nil {
99+
httpapi.InternalServerError(rw,err)
100+
return
101+
}
102+
iflen(resp)!=1 {
103+
httpapi.InternalServerError(rw,xerrors.Errorf("failed to serialize member to response, update still succeeded"))
104+
return
105+
}
106+
httpapi.Write(ctx,rw,http.StatusOK,resp[0])
91107
}
92108

93-
funcconvertOrganizationMember(mem database.OrganizationMember) codersdk.OrganizationMember {
94-
convertedMember:= codersdk.OrganizationMember{
95-
UserID:mem.UserID,
96-
OrganizationID:mem.OrganizationID,
97-
CreatedAt:mem.CreatedAt,
98-
UpdatedAt:mem.UpdatedAt,
99-
Roles:make([]codersdk.SlimRole,0,len(mem.Roles)),
109+
// convertOrganizationMembers batches the role lookup to make only 1 sql call
110+
// We
111+
funcconvertOrganizationMembers(ctx context.Context,db database.Store,mems []database.OrganizationMember) ([]codersdk.OrganizationMember,error) {
112+
converted:=make([]codersdk.OrganizationMember,0,len(mems))
113+
roleLookup:=make([]database.NameOrganizationPair,0)
114+
115+
for_,m:=rangemems {
116+
converted=append(converted, codersdk.OrganizationMember{
117+
UserID:m.UserID,
118+
OrganizationID:m.OrganizationID,
119+
CreatedAt:m.CreatedAt,
120+
UpdatedAt:m.UpdatedAt,
121+
Roles:db2sdk.List(m.Roles,func(rstring) codersdk.SlimRole {
122+
// If it is a built-in role, no lookups are needed.
123+
rbacRole,err:=rbac.RoleByName(rbac.RoleIdentifier{Name:r,OrganizationID:m.OrganizationID})
124+
iferr==nil {
125+
returndb2sdk.SlimRole(rbacRole)
126+
}
127+
128+
// We know the role name and the organization ID. We are missing the
129+
// display name. Append the lookup parameter, so we can get the display name
130+
roleLookup=append(roleLookup, database.NameOrganizationPair{
131+
Name:r,
132+
OrganizationID:m.OrganizationID,
133+
})
134+
return codersdk.SlimRole{
135+
Name:r,
136+
DisplayName:"",
137+
OrganizationID:m.OrganizationID.String(),
138+
}
139+
}),
140+
})
141+
}
142+
143+
customRoles,err:=db.CustomRoles(ctx, database.CustomRolesParams{
144+
LookupRoles:roleLookup,
145+
ExcludeOrgRoles:false,
146+
OrganizationID: uuid.UUID{},
147+
})
148+
iferr!=nil {
149+
// We are missing the display names, but that is not absolutely required. So just
150+
// return the converted and the names will be used instead of the display names.
151+
returnconverted,xerrors.Errorf("lookup custom roles: %w",err)
152+
}
153+
154+
// Now map the customRoles back to the slimRoles for their display name.
155+
customRolesMap:=make(map[string]database.CustomRole)
156+
for_,role:=rangecustomRoles {
157+
customRolesMap[role.RoleIdentifier().UniqueName()]=role
100158
}
101159

102-
for_,roleName:=rangemem.Roles {
103-
rbacRole,_:=rbac.RoleByName(rbac.RoleIdentifier{Name:roleName,OrganizationID:mem.OrganizationID})
104-
convertedMember.Roles=append(convertedMember.Roles,db2sdk.SlimRole(rbacRole))
160+
fori:=rangeconverted {
161+
forj,role:=rangeconverted[i].Roles {
162+
ifcr,ok:=customRolesMap[role.UniqueName()];ok {
163+
converted[i].Roles[j].DisplayName=cr.DisplayName
164+
}
165+
}
105166
}
106-
returnconvertedMember
167+
168+
returnconverted,nil
107169
}
108170

109-
funcconvertOrganizationMemberRow(row database.OrganizationMembersRow) codersdk.OrganizationMemberWithName {
110-
convertedMember:= codersdk.OrganizationMemberWithName{
111-
Username:row.Username,
112-
OrganizationMember:convertOrganizationMember(row.OrganizationMember),
171+
funcconvertOrganizationMemberRows(ctx context.Context,db database.Store,rows []database.OrganizationMembersRow) ([]codersdk.OrganizationMemberWithName,error) {
172+
members:=make([]database.OrganizationMember,0)
173+
for_,row:=rangerows {
174+
members=append(members,row.OrganizationMember)
175+
}
176+
177+
convertedMembers,err:=convertOrganizationMembers(ctx,db,members)
178+
iferr!=nil {
179+
returnnil,err
180+
}
181+
iflen(convertedMembers)!=len(rows) {
182+
returnnil,xerrors.Errorf("conversion failed, mismatch slice lengths")
183+
}
184+
185+
converted:=make([]codersdk.OrganizationMemberWithName,0)
186+
fori:=rangeconvertedMembers {
187+
converted=append(converted, codersdk.OrganizationMemberWithName{
188+
Username:rows[i].Username,
189+
OrganizationMember:convertedMembers[i],
190+
})
113191
}
114192

115-
returnconvertedMember
193+
returnconverted,nil
116194
}

‎coderd/rbac/roles.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ func (r RoleIdentifier) String() string {
9696
returnr.Name
9797
}
9898

99+
func (rRoleIdentifier)UniqueName()string {
100+
returnr.String()
101+
}
102+
99103
func (r*RoleIdentifier)MarshalJSON() ([]byte,error) {
100104
returnjson.Marshal(r.String())
101105
}

‎codersdk/organizations.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ type Organization struct {
5151
}
5252

5353
typeOrganizationMemberstruct {
54-
UserID uuid.UUID`db:"user_id" json:"user_id" format:"uuid"`
55-
OrganizationID uuid.UUID`db:"organization_id" json:"organization_id" format:"uuid"`
56-
CreatedAt time.Time`db:"created_at" json:"created_at" format:"date-time"`
57-
UpdatedAt time.Time`db:"updated_at" json:"updated_at" format:"date-time"`
58-
Roles []SlimRole`db:"roles" json:"roles"`
54+
UserID uuid.UUID`table:"user id" json:"user_id" format:"uuid"`
55+
OrganizationID uuid.UUID`table:"organization id" json:"organization_id" format:"uuid"`
56+
CreatedAt time.Time`table:"created at" json:"created_at" format:"date-time"`
57+
UpdatedAt time.Time`table:"updated at" json:"updated_at" format:"date-time"`
58+
Roles []SlimRole`table:"organization_roles" json:"roles"`
5959
}
6060

6161
typeOrganizationMemberWithNamestruct {

‎codersdk/roles.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,22 @@ type SlimRole struct {
1919
OrganizationIDstring`json:"organization_id,omitempty"`
2020
}
2121

22+
func (sSlimRole)String()string {
23+
ifs.DisplayName!="" {
24+
returns.DisplayName
25+
}
26+
returns.Name
27+
}
28+
29+
// UniqueName concatenates the organization ID to create a globally unique
30+
// string name for the role.
31+
func (sSlimRole)UniqueName()string {
32+
ifs.OrganizationID!="" {
33+
returns.Name+":"+s.OrganizationID
34+
}
35+
returns.Name
36+
}
37+
2238
typeAssignableRolesstruct {
2339
Role`table:"r,recursive_inline"`
2440
Assignablebool`json:"assignable" table:"assignable"`

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp