@@ -3,16 +3,27 @@ package iam
33import (
44 "context"
55 "fmt"
6+ "slices"
7+ "sort"
68 "strings"
9+ "time"
710
811 "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
912 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1013 iam "github.com/scaleway/scaleway-sdk-go/api/iam/v1alpha1"
1114 "github.com/scaleway/scaleway-sdk-go/scw"
1215 "github.com/scaleway/terraform-provider-scaleway/v2/internal/httperrors"
16+ "github.com/scaleway/terraform-provider-scaleway/v2/internal/transport"
1317 "github.com/scaleway/terraform-provider-scaleway/v2/internal/types"
1418)
1519
20+ type EntityKind string
21+
22+ const (
23+ EntityKindUser EntityKind = "user"
24+ EntityKindApplication EntityKind = "application"
25+ )
26+
1627func ResourceGroupMembership () * schema.Resource {
1728 return & schema.Resource {
1829 CreateContext : resourceIamGroupMembershipCreate ,
@@ -23,18 +34,20 @@ func ResourceGroupMembership() *schema.Resource {
2334 },
2435 SchemaVersion : 0 ,
2536 Schema : map [string ]* schema.Schema {
26- "user_id" : {
27- Type : schema .TypeString ,
37+ "user_ids" : {
38+ Type : schema .TypeList ,
39+ Elem : & schema.Schema {Type : schema .TypeString },
2840 Optional : true ,
29- Description : "The ID of the user" ,
30- ExactlyOneOf : [] string { "application_id" } ,
41+ ExactlyOneOf : [] string { "application_ids" } ,
42+ Description : "The IDs of the users" ,
3143 ForceNew : true ,
3244 },
33- "application_id" : {
34- Type : schema .TypeString ,
45+ "application_ids" : {
46+ Type : schema .TypeList ,
47+ Elem : & schema.Schema {Type : schema .TypeString },
3548 Optional : true ,
36- Description : "The ID of the user" ,
37- ExactlyOneOf : [] string { "user_id" } ,
49+ ExactlyOneOf : [] string { "user_ids" } ,
50+ Description : "The IDs of the applications" ,
3851 ForceNew : true ,
3952 },
4053 "group_id" : {
@@ -50,27 +63,27 @@ func ResourceGroupMembership() *schema.Resource {
5063func resourceIamGroupMembershipCreate (ctx context.Context , d * schema.ResourceData , m interface {}) diag.Diagnostics {
5164 api := NewAPI (m )
5265
53- userID := types .ExpandStringPtr (d .Get ("user_id " ))
54- applicationID := types .ExpandStringPtr (d .Get ("application_id " ))
66+ userIDs := types .ExpandStrings (d .Get ("user_ids " ))
67+ applicationIDs := types .ExpandStrings (d .Get ("application_ids " ))
5568
56- group , err := api . AddGroupMember ( & iam.AddGroupMemberRequest {
57- GroupID : d .Get ("group_id" ).(string ),
58- UserID : userID ,
59- ApplicationID : applicationID ,
60- }, scw . WithContext ( ctx ) )
69+ group , err := MakeSetGroupMembershipRequest ( ctx , api , & iam.SetGroupMembersRequest {
70+ GroupID : d .Get ("group_id" ).(string ),
71+ UserIDs : userIDs ,
72+ ApplicationIDs : applicationIDs ,
73+ })
6174 if err != nil {
6275 return diag .FromErr (err )
6376 }
6477
65- d .SetId (GroupMembershipID (group .ID , userID , applicationID ))
78+ d .SetId (SetGroupMembershipResourceID (group .ID , userIDs , applicationIDs ))
6679
6780 return resourceIamGroupMembershipRead (ctx , d , m )
6881}
6982
7083func resourceIamGroupMembershipRead (ctx context.Context , d * schema.ResourceData , m interface {}) diag.Diagnostics {
7184 api := NewAPI (m )
7285
73- groupID , userID , applicationID , err := ExpandGroupMembershipID (d .Id ())
86+ groupID , entityKind , entityIDs , err := ExpandGroupMembershipResourceID (d .Id ())
7487 if err != nil {
7588 return diag .FromErr (err )
7689 }
@@ -88,58 +101,46 @@ func resourceIamGroupMembershipRead(ctx context.Context, d *schema.ResourceData,
88101 return diag .FromErr (err )
89102 }
90103
91- foundInGroup := false
92-
93- if userID != "" {
94- for _ , groupUserID := range group .UserIDs {
95- if groupUserID == userID {
96- foundInGroup = true
104+ foundEntityIDs := make ([]bool , len (entityIDs ))
97105
98- break
106+ if entityKind == EntityKindUser {
107+ for i , groupUserID := range group .UserIDs {
108+ if slices .Contains (entityIDs , groupUserID ) {
109+ foundEntityIDs [i ] = true
99110 }
100111 }
101- } else if applicationID != "" {
102- for _ , groupApplicationID := range group .ApplicationIDs {
103- if groupApplicationID == applicationID {
104- foundInGroup = true
105-
106- break
112+ } else if entityKind == EntityKindApplication {
113+ for i , groupApplicationID := range group .ApplicationIDs {
114+ if slices .Contains (entityIDs , groupApplicationID ) {
115+ foundEntityIDs [i ] = true
107116 }
108117 }
109118 }
110119
111- if ! foundInGroup {
120+ if slices . Contains ( foundEntityIDs , false ) {
112121 d .SetId ("" )
113122
114123 return nil
115124 }
116125
117126 _ = d .Set ("group_id" , groupID )
118- _ = d .Set ("user_id" , userID )
119- _ = d .Set ("application_id" , applicationID )
127+ _ = d .Set (fmt .Sprintf ("%s_ids" , entityKind ), entityIDs )
120128
121129 return nil
122130}
123131
124132func resourceIamGroupMembershipDelete (ctx context.Context , d * schema.ResourceData , m interface {}) diag.Diagnostics {
125133 api := NewAPI (m )
126134
127- groupID , userID , applicationID , err := ExpandGroupMembershipID (d .Id ())
135+ groupID , _ , _ , err := ExpandGroupMembershipResourceID (d .Id ())
128136 if err != nil {
129137 return diag .FromErr (err )
130138 }
131139
132- req := & iam.RemoveGroupMemberRequest {
140+ _ , err = MakeSetGroupMembershipRequest ( ctx , api , & iam.SetGroupMembersRequest {
133141 GroupID : groupID ,
134- }
135-
136- if userID != "" {
137- req .UserID = & userID
138- } else if applicationID != "" {
139- req .ApplicationID = & applicationID
140- }
142+ })
141143
142- _ , err = api .RemoveGroupMember (req , scw .WithContext (ctx ))
143144 if err != nil {
144145 if httperrors .Is404 (err ) {
145146 d .SetId ("" )
@@ -153,28 +154,60 @@ func resourceIamGroupMembershipDelete(ctx context.Context, d *schema.ResourceDat
153154 return nil
154155}
155156
156- func GroupMembershipID (groupID string , userID * string , applicationID * string ) string {
157- if userID != nil {
158- return fmt .Sprintf ("%s/user/%s" , groupID , * userID )
157+ func SetGroupMembershipResourceID (groupID string , userIDs []string , applicationIDs []string ) (resourceID string ) {
158+ sort .Strings (userIDs )
159+ sort .Strings (applicationIDs )
160+
161+ if len (userIDs ) > 0 {
162+ resourceID = fmt .Sprintf ("%s/%s/%s" , groupID , EntityKindUser , strings .Join (userIDs , "," ))
163+ } else if len (applicationIDs ) > 0 {
164+ resourceID = fmt .Sprintf ("%s/%s/%s" , groupID , EntityKindApplication , strings .Join (applicationIDs , "," ))
159165 }
160166
161- return fmt . Sprintf ( "%s/app/%s" , groupID , * applicationID )
167+ return
162168}
163169
164- func ExpandGroupMembershipID (id string ) (groupID string , userID string , applicationID string , err error ) {
170+ func ExpandGroupMembershipResourceID (id string ) (groupID string , kind EntityKind , entityIDs [] string , err error ) {
165171 elems := strings .Split (id , "/" )
166172 if len (elems ) != 3 {
167- return "" , "" , "" , fmt .Errorf ("invalid group member id format, expected {groupID}/{type }/{memberID }, got: %s" , id )
173+ return "" , "" , [] string {} , fmt .Errorf ("invalid group membership id format, expected {groupID}/{entityKind }/{entityIDs }, got: %s" , id )
168174 }
169175
170176 groupID = elems [0 ]
171-
172- switch elems [1 ] {
173- case "user" :
174- userID = elems [2 ]
175- case "app" :
176- applicationID = elems [2 ]
177+ kind = EntityKind (elems [1 ])
178+ if kind != EntityKindUser && kind != EntityKindApplication {
179+ return "" , "" , []string {}, fmt .Errorf ("invalid entity kind, expected %s or %s, got: %s" , EntityKindUser , EntityKindApplication , kind )
177180 }
181+ entityIDs = strings .Split (elems [2 ], "," )
178182
179183 return
180184}
185+
186+ func MakeSetGroupMembershipRequest (ctx context.Context , api * iam.API , request * iam.SetGroupMembersRequest ) (* iam.Group , error ) {
187+ retryInterval := 250 * time .Millisecond
188+ maxRetries := 10
189+
190+ if transport .DefaultWaitRetryInterval != nil {
191+ retryInterval = * transport .DefaultWaitRetryInterval
192+ }
193+
194+ var response * iam.Group
195+ var err error
196+
197+ // exponential backoff
198+ for i := 0 ; i < maxRetries ; i ++ {
199+ response , err = api .SetGroupMembers (request , scw .WithContext (ctx ))
200+ if err != nil {
201+ if httperrors .Is409 (err ) && strings .Contains (err .Error (), fmt .Sprintf ("resource group with ID %s is in a transient state: updating" , request .GroupID )) {
202+ time .Sleep (retryInterval * time .Duration (i ))
203+ continue
204+ }
205+
206+ return nil , err
207+ }
208+
209+ return response , nil
210+ }
211+
212+ return nil , fmt .Errorf ("failed to set group membership after %d retries" , maxRetries )
213+ }
0 commit comments