5
5
"fmt"
6
6
"strings"
7
7
8
+ "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
8
9
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
10
+ "github.com/hashicorp/terraform-plugin-framework/attr"
9
11
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
10
12
"github.com/hashicorp/terraform-plugin-log/tflog"
11
13
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
@@ -18,6 +20,7 @@ import (
18
20
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
19
21
"github.com/hashicorp/terraform-plugin-framework/types"
20
22
"github.com/stackitcloud/stackit-sdk-go/core/config"
23
+ "github.com/stackitcloud/stackit-sdk-go/core/utils"
21
24
"github.com/stackitcloud/stackit-sdk-go/services/secretsmanager"
22
25
)
23
26
@@ -33,6 +36,7 @@ type Model struct {
33
36
InstanceId types.String `tfsdk:"instance_id"`
34
37
ProjectId types.String `tfsdk:"project_id"`
35
38
Name types.String `tfsdk:"name"`
39
+ ACLs types.List `tfsdk:"acls"`
36
40
}
37
41
38
42
// NewInstanceResource is a helper function to simplify the provider implementation.
@@ -94,6 +98,7 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
94
98
"instance_id" : "ID of the Secrets Manager instance." ,
95
99
"project_id" : "STACKIT project ID to which the instance is associated." ,
96
100
"name" : "Instance name." ,
101
+ "acls" : "The access control list for this instance. Each entry is an IP or IP range that is permitted to access, in CIDR notation" ,
97
102
}
98
103
99
104
resp .Schema = schema.Schema {
@@ -138,6 +143,17 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
138
143
stringvalidator .LengthAtLeast (1 ),
139
144
},
140
145
},
146
+ "acls" : schema.ListAttribute {
147
+ Description : descriptions ["acls" ],
148
+ ElementType : types .StringType ,
149
+ Optional : true ,
150
+ Validators : []validator.List {
151
+ listvalidator .UniqueValues (),
152
+ listvalidator .ValueStringsAre (
153
+ validate .CIDR (),
154
+ ),
155
+ },
156
+ },
141
157
},
142
158
}
143
159
}
@@ -153,6 +169,15 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
153
169
projectId := model .ProjectId .ValueString ()
154
170
ctx = tflog .SetField (ctx , "project_id" , projectId )
155
171
172
+ var acls []string
173
+ if ! (model .ACLs .IsNull () || model .ACLs .IsUnknown ()) {
174
+ diags = model .ACLs .ElementsAs (ctx , & acls , false )
175
+ resp .Diagnostics .Append (diags ... )
176
+ if resp .Diagnostics .HasError () {
177
+ return
178
+ }
179
+ }
180
+
156
181
// Generate API request body from model
157
182
payload , err := toCreatePayload (& model )
158
183
if err != nil {
@@ -168,8 +193,20 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
168
193
instanceId := * createResp .Id
169
194
ctx = tflog .SetField (ctx , "instance_id" , instanceId )
170
195
196
+ // Create ACLs
197
+ err = updateACLs (ctx , projectId , instanceId , acls , r .client )
198
+ if err != nil {
199
+ core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating instance" , fmt .Sprintf ("Creating ACLs: %v" , err ))
200
+ return
201
+ }
202
+ aclList , err := r .client .GetAcls (ctx , projectId , instanceId ).Execute ()
203
+ if err != nil {
204
+ core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating instance" , fmt .Sprintf ("Calling API for ACLs data: %v" , err ))
205
+ return
206
+ }
207
+
171
208
// Map response body to schema
172
- err = mapFields (createResp , & model )
209
+ err = mapFields (createResp , aclList , & model )
173
210
if err != nil {
174
211
core .LogAndAddError (ctx , & resp .Diagnostics , "Error creating instance" , fmt .Sprintf ("Processing API payload: %v" , err ))
175
212
return
@@ -202,9 +239,14 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
202
239
core .LogAndAddError (ctx , & resp .Diagnostics , "Error reading instance" , fmt .Sprintf ("Calling API: %v" , err ))
203
240
return
204
241
}
242
+ aclList , err := r .client .GetAcls (ctx , projectId , instanceId ).Execute ()
243
+ if err != nil {
244
+ core .LogAndAddError (ctx , & resp .Diagnostics , "Error reading instance" , fmt .Sprintf ("Calling API for ACLs data: %v" , err ))
245
+ return
246
+ }
205
247
206
248
// Map response body to schema
207
- err = mapFields (instanceResp , & model )
249
+ err = mapFields (instanceResp , aclList , & model )
208
250
if err != nil {
209
251
core .LogAndAddError (ctx , & resp .Diagnostics , "Error reading instance" , fmt .Sprintf ("Processing API payload: %v" , err ))
210
252
return
@@ -220,9 +262,58 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
220
262
}
221
263
222
264
// Update updates the resource and sets the updated Terraform state on success.
223
- func (r * instanceResource ) Update (ctx context.Context , _ resource.UpdateRequest , resp * resource.UpdateResponse ) { // nolint:gocritic // function signature required by Terraform
224
- // Update shouldn't be called
225
- core .LogAndAddError (ctx , & resp .Diagnostics , "Error updating instance" , "Instance can't be updated" )
265
+ func (r * instanceResource ) Update (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) { // nolint:gocritic // function signature required by Terraform
266
+ var model Model
267
+ diags := req .Plan .Get (ctx , & model )
268
+ resp .Diagnostics .Append (diags ... )
269
+ if resp .Diagnostics .HasError () {
270
+ return
271
+ }
272
+ projectId := model .ProjectId .ValueString ()
273
+ instanceId := model .InstanceId .ValueString ()
274
+ ctx = tflog .SetField (ctx , "project_id" , projectId )
275
+ ctx = tflog .SetField (ctx , "instance_id" , instanceId )
276
+
277
+ var acls []string
278
+ if ! (model .ACLs .IsNull () || model .ACLs .IsUnknown ()) {
279
+ diags = model .ACLs .ElementsAs (ctx , & acls , false )
280
+ resp .Diagnostics .Append (diags ... )
281
+ if resp .Diagnostics .HasError () {
282
+ return
283
+ }
284
+ }
285
+
286
+ // Update ACLs
287
+ err := updateACLs (ctx , projectId , instanceId , acls , r .client )
288
+ if err != nil {
289
+ core .LogAndAddError (ctx , & resp .Diagnostics , "Error updating instance" , fmt .Sprintf ("Updating ACLs: %v" , err ))
290
+ return
291
+ }
292
+
293
+ instanceResp , err := r .client .GetInstance (ctx , projectId , instanceId ).Execute ()
294
+ if err != nil {
295
+ core .LogAndAddError (ctx , & resp .Diagnostics , "Error updating instance" , fmt .Sprintf ("Calling API: %v" , err ))
296
+ return
297
+ }
298
+ aclList , err := r .client .GetAcls (ctx , projectId , instanceId ).Execute ()
299
+ if err != nil {
300
+ core .LogAndAddError (ctx , & resp .Diagnostics , "Error updating instance" , fmt .Sprintf ("Calling API for ACLs data: %v" , err ))
301
+ return
302
+ }
303
+
304
+ // Map response body to schema
305
+ err = mapFields (instanceResp , aclList , & model )
306
+ if err != nil {
307
+ core .LogAndAddError (ctx , & resp .Diagnostics , "Error updating instance" , fmt .Sprintf ("Processing API payload: %v" , err ))
308
+ return
309
+ }
310
+
311
+ diags = resp .State .Set (ctx , model )
312
+ resp .Diagnostics .Append (diags ... )
313
+ if resp .Diagnostics .HasError () {
314
+ return
315
+ }
316
+ tflog .Info (ctx , "Secrets Manager instance updated" )
226
317
}
227
318
228
319
// Delete deletes the resource and removes the Terraform state on success.
@@ -266,7 +357,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS
266
357
tflog .Info (ctx , "Secrets Manager instance state imported" )
267
358
}
268
359
269
- func mapFields (instance * secretsmanager.Instance , model * Model ) error {
360
+ func mapFields (instance * secretsmanager.Instance , aclList * secretsmanager. AclList , model * Model ) error {
270
361
if instance == nil {
271
362
return fmt .Errorf ("response input is nil" )
272
363
}
@@ -293,6 +384,32 @@ func mapFields(instance *secretsmanager.Instance, model *Model) error {
293
384
model .InstanceId = types .StringValue (instanceId )
294
385
model .Name = types .StringPointerValue (instance .Name )
295
386
387
+ err := mapACLs (aclList , model )
388
+ if err != nil {
389
+ return err
390
+ }
391
+
392
+ return nil
393
+ }
394
+
395
+ func mapACLs (aclList * secretsmanager.AclList , model * Model ) error {
396
+ if aclList == nil {
397
+ return fmt .Errorf ("nil ACL list" )
398
+ }
399
+ if aclList .Acls == nil || len (* aclList .Acls ) == 0 {
400
+ model .ACLs = types .ListNull (types .StringType )
401
+ return nil
402
+ }
403
+
404
+ acls := []attr.Value {}
405
+ for _ , acl := range * aclList .Acls {
406
+ acls = append (acls , types .StringValue (* acl .Cidr ))
407
+ }
408
+ aclsList , diags := types .ListValue (types .StringType , acls )
409
+ if diags .HasError () {
410
+ return fmt .Errorf ("mapping ACLs: %w" , core .DiagsToError (diags ))
411
+ }
412
+ model .ACLs = aclsList
296
413
return nil
297
414
}
298
415
@@ -304,3 +421,54 @@ func toCreatePayload(model *Model) (*secretsmanager.CreateInstancePayload, error
304
421
Name : model .Name .ValueStringPointer (),
305
422
}, nil
306
423
}
424
+
425
+ // updateACLs creates and deletes ACLs so that the instance's ACLs are the ones in the model
426
+ func updateACLs (ctx context.Context , projectId , instanceId string , acls []string , client * secretsmanager.APIClient ) error {
427
+ // Get ACLs current state
428
+ currentACLsResp , err := client .GetAcls (ctx , projectId , instanceId ).Execute ()
429
+ if err != nil {
430
+ return fmt .Errorf ("fetching current ACLs: %w" , err )
431
+ }
432
+
433
+ type aclState struct {
434
+ isInModel bool
435
+ isCreated bool
436
+ id string
437
+ }
438
+ aclsState := make (map [string ]* aclState )
439
+ for _ , cidr := range acls {
440
+ aclsState [cidr ] = & aclState {
441
+ isInModel : true ,
442
+ }
443
+ }
444
+ for _ , acl := range * currentACLsResp .Acls {
445
+ cidr := * acl .Cidr
446
+ if _ , ok := aclsState [cidr ]; ! ok {
447
+ aclsState [cidr ] = & aclState {}
448
+ }
449
+ aclsState [cidr ].isCreated = true
450
+ aclsState [cidr ].id = * acl .Id
451
+ }
452
+
453
+ // Create/delete ACLs
454
+ for cidr , state := range aclsState {
455
+ if state .isInModel && ! state .isCreated {
456
+ payload := secretsmanager.CreateAclPayload {
457
+ Cidr : utils .Ptr (cidr ),
458
+ }
459
+ _ , err := client .CreateAcl (ctx , projectId , instanceId ).CreateAclPayload (payload ).Execute ()
460
+ if err != nil {
461
+ return fmt .Errorf ("creating ACL '%v': %w" , cidr , err )
462
+ }
463
+ }
464
+
465
+ if ! state .isInModel && state .isCreated {
466
+ err := client .DeleteAcl (ctx , projectId , instanceId , state .id ).Execute ()
467
+ if err != nil {
468
+ return fmt .Errorf ("deleting ACL '%v': %w" , cidr , err )
469
+ }
470
+ }
471
+ }
472
+
473
+ return nil
474
+ }
0 commit comments