Skip to content

Commit e126557

Browse files
hcsa73Henrique Santos
andauthored
Implement Secrets Manager ACL (#93)
* Add CIDR validator * Implement `syncACL`, add it to creation * Rename function * Rename variables * Add mapACLs * Implement instance update * Add ACLs to acc test * Add ACL to schema * Add new line * Fix not using the ACLs read from config * Add test case where ACLs aren't set * Fix lint * Generate docs * Add uniqueness check for ACLs * Add repeated ACLs test cases * Remove debug leftover * Change test cases * Rename data * Add ACL description * Generate docs * Change ACL attribute type * Remove test case --------- Co-authored-by: Henrique Santos <[email protected]>
1 parent 3c67485 commit e126557

File tree

10 files changed

+770
-13
lines changed

10 files changed

+770
-13
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "stackit_secretsmanager_instance Data Source - stackit"
4+
subcategory: ""
5+
description: |-
6+
Secrets Manager instance data source schema. Must have a region specified in the provider configuration.
7+
---
8+
9+
# stackit_secretsmanager_instance (Data Source)
10+
11+
Secrets Manager instance data source schema. Must have a `region` specified in the provider configuration.
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Required
19+
20+
- `instance_id` (String) ID of the Secrets Manager instance.
21+
- `project_id` (String) STACKIT project ID to which the instance is associated.
22+
23+
### Read-Only
24+
25+
- `acls` (List of String) The access control list for this instance. Each entry is an IP or IP range that is permitted to access, in CIDR notation
26+
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`".
27+
- `name` (String) Instance name.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "stackit_secretsmanager_instance Resource - stackit"
4+
subcategory: ""
5+
description: |-
6+
Secrets Manager instance resource schema. Must have a region specified in the provider configuration.
7+
---
8+
9+
# stackit_secretsmanager_instance (Resource)
10+
11+
Secrets Manager instance resource schema. Must have a `region` specified in the provider configuration.
12+
13+
14+
15+
<!-- schema generated by tfplugindocs -->
16+
## Schema
17+
18+
### Required
19+
20+
- `name` (String) Instance name.
21+
- `project_id` (String) STACKIT project ID to which the instance is associated.
22+
23+
### Optional
24+
25+
- `acls` (List of String) The access control list for this instance. Each entry is an IP or IP range that is permitted to access, in CIDR notation
26+
27+
### Read-Only
28+
29+
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`".
30+
- `instance_id` (String) ID of the Secrets Manager instance.

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.20
55
require (
66
github.com/google/go-cmp v0.6.0
77
github.com/google/uuid v1.3.1
8+
github.com/gorilla/mux v1.8.0
89
github.com/hashicorp/terraform-plugin-framework v1.4.1
910
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
1011
github.com/hashicorp/terraform-plugin-go v0.19.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
4242
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
4343
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
4444
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
45+
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
46+
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
4547
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
4648
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
4749
github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU=

stackit/internal/services/secretsmanager/instance/datasource.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/hashicorp/terraform-plugin-framework/datasource"
88
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
9+
"github.com/hashicorp/terraform-plugin-framework/types"
910
"github.com/hashicorp/terraform-plugin-log/tflog"
1011
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
1112
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
@@ -79,6 +80,7 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques
7980
"instance_id": "ID of the Secrets Manager instance.",
8081
"project_id": "STACKIT project ID to which the instance is associated.",
8182
"name": "Instance name.",
83+
"acls": "The access control list for this instance. Each entry is an IP or IP range that is permitted to access, in CIDR notation",
8284
}
8385

8486
resp.Schema = schema.Schema{
@@ -108,6 +110,11 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques
108110
Description: descriptions["name"],
109111
Computed: true,
110112
},
113+
"acls": schema.ListAttribute{
114+
Description: descriptions["acls"],
115+
ElementType: types.StringType,
116+
Computed: true,
117+
},
111118
},
112119
}
113120
}
@@ -130,8 +137,13 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques
130137
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err))
131138
return
132139
}
140+
aclList, err := r.client.GetAcls(ctx, projectId, instanceId).Execute()
141+
if err != nil {
142+
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API for ACLs data: %v", err))
143+
return
144+
}
133145

134-
err = mapFields(instanceResp, &model)
146+
err = mapFields(instanceResp, aclList, &model)
135147
if err != nil {
136148
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err))
137149
return

stackit/internal/services/secretsmanager/instance/resource.go

Lines changed: 174 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"fmt"
66
"strings"
77

8+
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
89
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
10+
"github.com/hashicorp/terraform-plugin-framework/attr"
911
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
1012
"github.com/hashicorp/terraform-plugin-log/tflog"
1113
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
@@ -18,6 +20,7 @@ import (
1820
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1921
"github.com/hashicorp/terraform-plugin-framework/types"
2022
"github.com/stackitcloud/stackit-sdk-go/core/config"
23+
"github.com/stackitcloud/stackit-sdk-go/core/utils"
2124
"github.com/stackitcloud/stackit-sdk-go/services/secretsmanager"
2225
)
2326

@@ -33,6 +36,7 @@ type Model struct {
3336
InstanceId types.String `tfsdk:"instance_id"`
3437
ProjectId types.String `tfsdk:"project_id"`
3538
Name types.String `tfsdk:"name"`
39+
ACLs types.List `tfsdk:"acls"`
3640
}
3741

3842
// NewInstanceResource is a helper function to simplify the provider implementation.
@@ -94,6 +98,7 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
9498
"instance_id": "ID of the Secrets Manager instance.",
9599
"project_id": "STACKIT project ID to which the instance is associated.",
96100
"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",
97102
}
98103

99104
resp.Schema = schema.Schema{
@@ -138,6 +143,17 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
138143
stringvalidator.LengthAtLeast(1),
139144
},
140145
},
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+
},
141157
},
142158
}
143159
}
@@ -153,6 +169,15 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
153169
projectId := model.ProjectId.ValueString()
154170
ctx = tflog.SetField(ctx, "project_id", projectId)
155171

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+
156181
// Generate API request body from model
157182
payload, err := toCreatePayload(&model)
158183
if err != nil {
@@ -168,8 +193,20 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
168193
instanceId := *createResp.Id
169194
ctx = tflog.SetField(ctx, "instance_id", instanceId)
170195

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+
171208
// Map response body to schema
172-
err = mapFields(createResp, &model)
209+
err = mapFields(createResp, aclList, &model)
173210
if err != nil {
174211
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err))
175212
return
@@ -202,9 +239,14 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
202239
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err))
203240
return
204241
}
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+
}
205247

206248
// Map response body to schema
207-
err = mapFields(instanceResp, &model)
249+
err = mapFields(instanceResp, aclList, &model)
208250
if err != nil {
209251
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err))
210252
return
@@ -220,9 +262,58 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
220262
}
221263

222264
// 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")
226317
}
227318

228319
// 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
266357
tflog.Info(ctx, "Secrets Manager instance state imported")
267358
}
268359

269-
func mapFields(instance *secretsmanager.Instance, model *Model) error {
360+
func mapFields(instance *secretsmanager.Instance, aclList *secretsmanager.AclList, model *Model) error {
270361
if instance == nil {
271362
return fmt.Errorf("response input is nil")
272363
}
@@ -293,6 +384,32 @@ func mapFields(instance *secretsmanager.Instance, model *Model) error {
293384
model.InstanceId = types.StringValue(instanceId)
294385
model.Name = types.StringPointerValue(instance.Name)
295386

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
296413
return nil
297414
}
298415

@@ -304,3 +421,54 @@ func toCreatePayload(model *Model) (*secretsmanager.CreateInstancePayload, error
304421
Name: model.Name.ValueStringPointer(),
305422
}, nil
306423
}
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

Comments
 (0)