Skip to content

Commit fda328c

Browse files
authored
feat: add effective role to sentry_team_member resource (#372)
1 parent df5b567 commit fda328c

8 files changed

+261
-80
lines changed

docs/resources/team_member.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ resource "sentry_team_member" "default" {
4848

4949
### Read-Only
5050

51+
- `effective_role` (String) The effective role of the member in the team. This represents the highest role, determined by comparing the lower role assigned by the member's organizational role with the role assigned by the member's team role.
5152
- `id` (String) The ID of this resource.
5253

5354
## Import

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/hashicorp/terraform-plugin-log v0.9.0
1313
github.com/hashicorp/terraform-plugin-mux v0.13.0
1414
github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0
15-
github.com/jianyuan/go-sentry/v2 v2.6.2
15+
github.com/jianyuan/go-sentry/v2 v2.7.0
1616
golang.org/x/oauth2 v0.15.0
1717
golang.org/x/sync v0.6.0
1818
)

go.sum

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
133133
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
134134
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
135135
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
136-
github.com/jianyuan/go-sentry/v2 v2.6.2 h1:tMcsFMjON1IvOtpW1KxZmfdRbW7IXUHY7Kwk6Rmi7oY=
137-
github.com/jianyuan/go-sentry/v2 v2.6.2/go.mod h1:YN4yg9u6/b5TfsmXy8OauRu3cyvVXRe1mJY7Pe+/Dt4=
136+
github.com/jianyuan/go-sentry/v2 v2.7.0 h1:gAJLbEzyZJJDZbeXM4OMqpnzNi0+zwTvpAxKsbEvmEs=
137+
github.com/jianyuan/go-sentry/v2 v2.7.0/go.mod h1:YN4yg9u6/b5TfsmXy8OauRu3cyvVXRe1mJY7Pe+/Dt4=
138138
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
139139
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
140140
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -245,8 +245,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
245245
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
246246
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
247247
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
248-
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
249-
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
250248
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
251249
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
252250
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

internal/provider/data_source_organization_member.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func (m *OrganizationMemberDataSourceModel) Fill(organization string, d sentry.O
3131
m.Id = types.StringValue(d.ID)
3232
m.Organization = types.StringValue(organization)
3333
m.Email = types.StringValue(d.Email)
34-
m.Role = types.StringValue(d.OrganizationRole)
34+
m.Role = types.StringValue(d.OrgRole)
3535

3636
return nil
3737
}

internal/provider/resource_team_member.go

Lines changed: 140 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
package provider
22

33
import (
4+
"cmp"
45
"context"
56
"fmt"
7+
"slices"
68
"sync"
79

10+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
811
"github.com/hashicorp/terraform-plugin-framework/path"
912
"github.com/hashicorp/terraform-plugin-framework/resource"
1013
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
1114
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1215
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
16+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
1317
"github.com/hashicorp/terraform-plugin-framework/types"
1418
"github.com/jianyuan/go-sentry/v2/sentry"
1519
)
@@ -28,19 +32,21 @@ type TeamMemberResource struct {
2832
}
2933

3034
type TeamMemberResourceModel struct {
31-
Id types.String `tfsdk:"id"`
32-
Organization types.String `tfsdk:"organization"`
33-
MemberId types.String `tfsdk:"member_id"`
34-
Team types.String `tfsdk:"team"`
35-
Role types.String `tfsdk:"role"`
35+
Id types.String `tfsdk:"id"`
36+
Organization types.String `tfsdk:"organization"`
37+
MemberId types.String `tfsdk:"member_id"`
38+
Team types.String `tfsdk:"team"`
39+
Role types.String `tfsdk:"role"`
40+
EffectiveRole types.String `tfsdk:"effective_role"`
3641
}
3742

38-
func (data *TeamMemberResourceModel) Fill(organization string, team string, memberId string, role string) error {
43+
func (data *TeamMemberResourceModel) Fill(organization string, team string, memberId string, role *string, effectiveRole string) error {
3944
data.Id = types.StringValue(buildThreePartID(organization, team, memberId))
4045
data.Organization = types.StringValue(organization)
4146
data.MemberId = types.StringValue(memberId)
4247
data.Team = types.StringValue(team)
43-
data.Role = types.StringValue(role)
48+
data.Role = types.StringPointerValue(role)
49+
data.EffectiveRole = types.StringValue(effectiveRole)
4450

4551
return nil
4652
}
@@ -82,6 +88,13 @@ func (r *TeamMemberResource) Schema(ctx context.Context, req resource.SchemaRequ
8288
"role": schema.StringAttribute{
8389
Description: "The role of the member in the team. When not set, resolve to the minimum team role given by this member's organization role.",
8490
Optional: true,
91+
Validators: []validator.String{
92+
stringvalidator.OneOf("contributor", "admin"),
93+
},
94+
},
95+
"effective_role": schema.StringAttribute{
96+
Description: "The effective role of the member in the team. This represents the highest role, determined by comparing the lower role assigned by the member's organizational role with the role assigned by the member's team role.",
97+
Computed: true,
8598
},
8699
},
87100
}
@@ -107,70 +120,115 @@ func (r *TeamMemberResource) Configure(ctx context.Context, req resource.Configu
107120
r.client = client
108121
}
109122

110-
func (r *TeamMemberResource) readRole(ctx context.Context, organization string, memberId string, team string) (*string, error) {
123+
func getEffectiveOrgRole(memberOrgRoles []string, orgRoleList []sentry.OrganizationRoleListItem) *sentry.OrganizationRoleListItem {
124+
orgRoleMap := make(map[string]struct {
125+
index int
126+
role sentry.OrganizationRoleListItem
127+
}, len(orgRoleList))
128+
for i, role := range orgRoleList {
129+
orgRoleMap[role.ID] = struct {
130+
index int
131+
role sentry.OrganizationRoleListItem
132+
}{
133+
index: i,
134+
role: role,
135+
}
136+
}
137+
memberOrgRolesCopy := make([]string, len(memberOrgRoles))
138+
copy(memberOrgRolesCopy, memberOrgRoles)
139+
140+
slices.SortFunc(memberOrgRolesCopy, func(i, j string) int {
141+
return cmp.Compare(orgRoleMap[j].index, orgRoleMap[i].index)
142+
})
143+
144+
if len(memberOrgRolesCopy) > 0 {
145+
if orgRoleMap, ok := orgRoleMap[memberOrgRolesCopy[0]]; ok {
146+
return &orgRoleMap.role
147+
}
148+
}
149+
150+
return nil
151+
}
152+
153+
func hasOrgRoleOverwrite(orgRole *sentry.OrganizationRoleListItem, orgRoleList []sentry.OrganizationRoleListItem, teamRoleList []sentry.TeamRoleListItem) bool {
154+
if orgRole == nil {
155+
return false
156+
}
157+
158+
teamRoleIndex := slices.IndexFunc(teamRoleList, func(teamRole sentry.TeamRoleListItem) bool {
159+
return teamRole.ID == orgRole.MinimumTeamRole
160+
})
161+
162+
return teamRoleIndex > 0
163+
}
164+
165+
// Adapted from https://github.com/getsentry/sentry/blob/23.12.1/static/app/components/teamRoleSelect.tsx#L30-L69
166+
func (r *TeamMemberResource) getEffectiveTeamRole(ctx context.Context, organization string, memberId string, teamSlug string) (*string, error) {
111167
r.roleMu.Lock()
112168
defer r.roleMu.Unlock()
113169

170+
org, _, err := r.client.Organizations.Get(ctx, organization)
171+
if err != nil {
172+
return nil, fmt.Errorf("unable to read organization, got error: %s", err)
173+
}
174+
175+
team, _, err := r.client.Teams.Get(ctx, organization, teamSlug)
176+
if err != nil {
177+
return nil, fmt.Errorf("unable to read team, got error: %s", err)
178+
}
179+
114180
member, _, err := r.client.OrganizationMembers.Get(ctx, organization, memberId)
115181
if err != nil {
116182
return nil, fmt.Errorf("unable to read organization member, got error: %s", err)
117183
}
118184

119-
teamRole := r.readTeamRole(member.TeamRoles, team)
120-
if teamRole == nil {
121-
return nil, fmt.Errorf("unable to find team member")
185+
possibleOrgRoles := []string{member.OrgRole}
186+
if team.OrgRole != nil {
187+
possibleOrgRoles = append(possibleOrgRoles, sentry.StringValue(team.OrgRole))
122188
}
123189

124-
return teamRole, nil
125-
}
190+
effectiveOrgRole := getEffectiveOrgRole(possibleOrgRoles, org.OrgRoleList)
126191

127-
func (r *TeamMemberResource) readTeamRole(teamRoles []sentry.TeamRole, team string) *string {
128-
for _, teamRole := range teamRoles {
129-
if teamRole.TeamSlug == team {
130-
return &teamRole.Role
192+
if hasOrgRoleOverwrite(effectiveOrgRole, org.OrgRoleList, org.TeamRoleList) {
193+
teamRoleIndex := slices.IndexFunc(org.TeamRoleList, func(teamRole sentry.TeamRoleListItem) bool {
194+
return teamRole.ID == effectiveOrgRole.MinimumTeamRole
195+
})
196+
if teamRoleIndex != -1 {
197+
teamRole := org.TeamRoleList[teamRoleIndex]
198+
return &teamRole.ID, nil
131199
}
132200
}
133201

134-
return nil
202+
teamRoleIndex := slices.IndexFunc(member.TeamRoles, func(teamRole sentry.TeamRole) bool {
203+
return teamRole.TeamSlug == teamSlug
204+
})
205+
if teamRoleIndex != -1 {
206+
teamRole := member.TeamRoles[teamRoleIndex]
207+
if teamRole.Role != nil {
208+
return teamRole.Role, nil
209+
}
210+
}
211+
212+
teamRole := member.TeamRoleList[0]
213+
return &teamRole.ID, nil
135214
}
136215

137216
func (r *TeamMemberResource) updateRole(ctx context.Context, organization string, memberId string, team string, role string) (*string, error) {
138217
r.roleMu.Lock()
139218
defer r.roleMu.Unlock()
140219

141-
orgMember, _, err := r.client.OrganizationMembers.Get(ctx, organization, memberId)
220+
member, _, err := r.client.TeamMembers.Update(ctx, organization, memberId, team, &sentry.UpdateTeamMemberParams{
221+
TeamRole: sentry.String(role),
222+
})
142223
if err != nil {
143224
return nil, fmt.Errorf("unable to read organization member, got error: %s", err)
144225
}
145226

146-
teamRoles := make([]sentry.TeamRole, 0, len(orgMember.TeamRoles))
147-
for _, teamRole := range orgMember.TeamRoles {
148-
if teamRole.TeamSlug == team {
149-
teamRole.Role = role
150-
}
151-
teamRoles = append(teamRoles, teamRole)
152-
}
153-
154-
orgMember, _, err = r.client.OrganizationMembers.Update(
155-
ctx,
156-
organization,
157-
memberId,
158-
&sentry.UpdateOrganizationMemberParams{
159-
OrganizationRole: orgMember.OrganizationRole,
160-
TeamRoles: teamRoles,
161-
},
162-
)
163-
if err != nil {
164-
return nil, fmt.Errorf("unable to update organization member's team role, got error: %s", err)
165-
}
166-
167-
for _, teamRole := range orgMember.TeamRoles {
168-
if teamRole.TeamSlug == team {
169-
return &teamRole.Role, nil
170-
}
227+
if !sentry.BoolValue(member.IsActive) {
228+
return nil, fmt.Errorf("team member is not active")
171229
}
172230

173-
return nil, fmt.Errorf("unable to find team member")
231+
return member.TeamRole, nil
174232
}
175233

176234
func (r *TeamMemberResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
@@ -183,7 +241,7 @@ func (r *TeamMemberResource) Create(ctx context.Context, req resource.CreateRequ
183241
return
184242
}
185243

186-
member, _, err := r.client.TeamMembers.Create(
244+
_, _, err := r.client.TeamMembers.Create(
187245
ctx,
188246
data.Organization.ValueString(),
189247
data.MemberId.ValueString(),
@@ -194,23 +252,28 @@ func (r *TeamMemberResource) Create(ctx context.Context, req resource.CreateRequ
194252
return
195253
}
196254

197-
var teamRole *string
198-
if data.Role.IsNull() {
199-
teamRole = member.TeamRole
200-
} else {
201-
teamRole, err = r.updateRole(ctx, data.Organization.ValueString(), data.MemberId.ValueString(), data.Team.ValueString(), data.Role.ValueString())
255+
if !data.Role.IsNull() {
256+
_, err = r.updateRole(ctx, data.Organization.ValueString(), data.MemberId.ValueString(), data.Team.ValueString(), data.Role.ValueString())
202257
if err != nil {
203258
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read team member, got error: %s", err))
204259
return
205260
}
206261
}
207262

208-
if teamRole == nil {
209-
resp.Diagnostics.AddError("Client Error", "Unable to find team member")
263+
effectiveRole, err := r.getEffectiveTeamRole(ctx, data.Organization.ValueString(), data.MemberId.ValueString(), data.Team.ValueString())
264+
if err != nil {
265+
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read team member role, got error: %s", err))
266+
resp.State.RemoveResource(ctx)
210267
return
211268
}
212269

213-
if err := data.Fill(data.Organization.ValueString(), data.Team.ValueString(), data.MemberId.ValueString(), *teamRole); err != nil {
270+
if err := data.Fill(
271+
data.Organization.ValueString(),
272+
data.Team.ValueString(),
273+
data.MemberId.ValueString(),
274+
data.Role.ValueStringPointer(),
275+
*effectiveRole,
276+
); err != nil {
214277
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to fill team member, got error: %s", err))
215278
return
216279
}
@@ -229,14 +292,20 @@ func (r *TeamMemberResource) Read(ctx context.Context, req resource.ReadRequest,
229292
return
230293
}
231294

232-
role, err := r.readRole(ctx, data.Organization.ValueString(), data.MemberId.ValueString(), data.Team.ValueString())
295+
effectiveRole, err := r.getEffectiveTeamRole(ctx, data.Organization.ValueString(), data.MemberId.ValueString(), data.Team.ValueString())
233296
if err != nil {
234297
resp.Diagnostics.AddError("Client Error", err.Error())
235298
resp.State.RemoveResource(ctx)
236299
return
237300
}
238301

239-
if err := data.Fill(data.Organization.ValueString(), data.Team.ValueString(), data.MemberId.ValueString(), *role); err != nil {
302+
if err := data.Fill(
303+
data.Organization.ValueString(),
304+
data.Team.ValueString(),
305+
data.MemberId.ValueString(),
306+
data.Role.ValueStringPointer(),
307+
*effectiveRole,
308+
); err != nil {
240309
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to fill team member, got error: %s", err))
241310
return
242311
}
@@ -257,13 +326,26 @@ func (r *TeamMemberResource) Update(ctx context.Context, req resource.UpdateRequ
257326

258327
// Update the role if it has changed
259328
if !plan.Role.Equal(state.Role) {
260-
role, err := r.updateRole(ctx, plan.Organization.ValueString(), plan.MemberId.ValueString(), plan.Team.ValueString(), plan.Role.ValueString())
329+
_, err := r.updateRole(ctx, plan.Organization.ValueString(), plan.MemberId.ValueString(), plan.Team.ValueString(), plan.Role.ValueString())
330+
if err != nil {
331+
resp.Diagnostics.AddError("Client Error", err.Error())
332+
return
333+
}
334+
335+
effectiveRole, err := r.getEffectiveTeamRole(ctx, plan.Organization.ValueString(), plan.MemberId.ValueString(), plan.Team.ValueString())
261336
if err != nil {
262337
resp.Diagnostics.AddError("Client Error", err.Error())
338+
resp.State.RemoveResource(ctx)
263339
return
264340
}
265341

266-
if err := state.Fill(plan.Organization.ValueString(), plan.Team.ValueString(), plan.MemberId.ValueString(), *role); err != nil {
342+
if err := state.Fill(
343+
plan.Organization.ValueString(),
344+
plan.Team.ValueString(),
345+
plan.MemberId.ValueString(),
346+
plan.Role.ValueStringPointer(),
347+
*effectiveRole,
348+
); err != nil {
267349
resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to fill team member, got error: %s", err))
268350
return
269351
}

0 commit comments

Comments
 (0)