1
1
package provider
2
2
3
3
import (
4
+ "cmp"
4
5
"context"
5
6
"fmt"
7
+ "slices"
6
8
"sync"
7
9
10
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
8
11
"github.com/hashicorp/terraform-plugin-framework/path"
9
12
"github.com/hashicorp/terraform-plugin-framework/resource"
10
13
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
11
14
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
12
15
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
16
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
13
17
"github.com/hashicorp/terraform-plugin-framework/types"
14
18
"github.com/jianyuan/go-sentry/v2/sentry"
15
19
)
@@ -28,19 +32,21 @@ type TeamMemberResource struct {
28
32
}
29
33
30
34
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"`
36
41
}
37
42
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 {
39
44
data .Id = types .StringValue (buildThreePartID (organization , team , memberId ))
40
45
data .Organization = types .StringValue (organization )
41
46
data .MemberId = types .StringValue (memberId )
42
47
data .Team = types .StringValue (team )
43
- data .Role = types .StringValue (role )
48
+ data .Role = types .StringPointerValue (role )
49
+ data .EffectiveRole = types .StringValue (effectiveRole )
44
50
45
51
return nil
46
52
}
@@ -82,6 +88,13 @@ func (r *TeamMemberResource) Schema(ctx context.Context, req resource.SchemaRequ
82
88
"role" : schema.StringAttribute {
83
89
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." ,
84
90
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 ,
85
98
},
86
99
},
87
100
}
@@ -107,70 +120,115 @@ func (r *TeamMemberResource) Configure(ctx context.Context, req resource.Configu
107
120
r .client = client
108
121
}
109
122
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 ) {
111
167
r .roleMu .Lock ()
112
168
defer r .roleMu .Unlock ()
113
169
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
+
114
180
member , _ , err := r .client .OrganizationMembers .Get (ctx , organization , memberId )
115
181
if err != nil {
116
182
return nil , fmt .Errorf ("unable to read organization member, got error: %s" , err )
117
183
}
118
184
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 ) )
122
188
}
123
189
124
- return teamRole , nil
125
- }
190
+ effectiveOrgRole := getEffectiveOrgRole (possibleOrgRoles , org .OrgRoleList )
126
191
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
131
199
}
132
200
}
133
201
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
135
214
}
136
215
137
216
func (r * TeamMemberResource ) updateRole (ctx context.Context , organization string , memberId string , team string , role string ) (* string , error ) {
138
217
r .roleMu .Lock ()
139
218
defer r .roleMu .Unlock ()
140
219
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
+ })
142
223
if err != nil {
143
224
return nil , fmt .Errorf ("unable to read organization member, got error: %s" , err )
144
225
}
145
226
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" )
171
229
}
172
230
173
- return nil , fmt . Errorf ( "unable to find team member" )
231
+ return member . TeamRole , nil
174
232
}
175
233
176
234
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
183
241
return
184
242
}
185
243
186
- member , _ , err := r .client .TeamMembers .Create (
244
+ _ , _ , err := r .client .TeamMembers .Create (
187
245
ctx ,
188
246
data .Organization .ValueString (),
189
247
data .MemberId .ValueString (),
@@ -194,23 +252,28 @@ func (r *TeamMemberResource) Create(ctx context.Context, req resource.CreateRequ
194
252
return
195
253
}
196
254
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 ())
202
257
if err != nil {
203
258
resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to read team member, got error: %s" , err ))
204
259
return
205
260
}
206
261
}
207
262
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 )
210
267
return
211
268
}
212
269
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 {
214
277
resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to fill team member, got error: %s" , err ))
215
278
return
216
279
}
@@ -229,14 +292,20 @@ func (r *TeamMemberResource) Read(ctx context.Context, req resource.ReadRequest,
229
292
return
230
293
}
231
294
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 ())
233
296
if err != nil {
234
297
resp .Diagnostics .AddError ("Client Error" , err .Error ())
235
298
resp .State .RemoveResource (ctx )
236
299
return
237
300
}
238
301
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 {
240
309
resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to fill team member, got error: %s" , err ))
241
310
return
242
311
}
@@ -257,13 +326,26 @@ func (r *TeamMemberResource) Update(ctx context.Context, req resource.UpdateRequ
257
326
258
327
// Update the role if it has changed
259
328
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 ())
261
336
if err != nil {
262
337
resp .Diagnostics .AddError ("Client Error" , err .Error ())
338
+ resp .State .RemoveResource (ctx )
263
339
return
264
340
}
265
341
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 {
267
349
resp .Diagnostics .AddError ("Client Error" , fmt .Sprintf ("Unable to fill team member, got error: %s" , err ))
268
350
return
269
351
}
0 commit comments