Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/data-sources/secretsmanager_instance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_secretsmanager_instance Data Source - stackit"
subcategory: ""
description: |-
Secrets Manager instance data source schema. Must have a region specified in the provider configuration.
---

# stackit_secretsmanager_instance (Data Source)

Secrets Manager instance data source schema. Must have a `region` specified in the provider configuration.



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `instance_id` (String) ID of the Secrets Manager instance.
- `project_id` (String) STACKIT project ID to which the instance is associated.

### Read-Only

- `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
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`".
- `name` (String) Instance name.
30 changes: 30 additions & 0 deletions docs/resources/secretsmanager_instance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_secretsmanager_instance Resource - stackit"
subcategory: ""
description: |-
Secrets Manager instance resource schema. Must have a region specified in the provider configuration.
---

# stackit_secretsmanager_instance (Resource)

Secrets Manager instance resource schema. Must have a `region` specified in the provider configuration.



<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) Instance name.
- `project_id` (String) STACKIT project ID to which the instance is associated.

### Optional

- `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

### Read-Only

- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`instance_id`".
- `instance_id` (String) ID of the Secrets Manager instance.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.20
require (
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.3.1
github.com/gorilla/mux v1.8.0
github.com/hashicorp/terraform-plugin-framework v1.4.1
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
github.com/hashicorp/terraform-plugin-go v0.19.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

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

resp.Schema = schema.Schema{
Expand Down Expand Up @@ -108,6 +110,11 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques
Description: descriptions["name"],
Computed: true,
},
"acls": schema.ListAttribute{
Description: descriptions["acls"],
ElementType: types.StringType,
Computed: true,
},
},
}
}
Expand All @@ -130,8 +137,13 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err))
return
}
aclList, err := r.client.GetAcls(ctx, projectId, instanceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API for ACLs data: %v", err))
return
}

err = mapFields(instanceResp, &model)
err = mapFields(instanceResp, aclList, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Processing API payload: %v", err))
return
Expand Down
180 changes: 174 additions & 6 deletions stackit/internal/services/secretsmanager/instance/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
Expand All @@ -18,6 +20,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/secretsmanager"
)

Expand All @@ -33,6 +36,7 @@ type Model struct {
InstanceId types.String `tfsdk:"instance_id"`
ProjectId types.String `tfsdk:"project_id"`
Name types.String `tfsdk:"name"`
ACLs types.List `tfsdk:"acls"`
}

// NewInstanceResource is a helper function to simplify the provider implementation.
Expand Down Expand Up @@ -94,6 +98,7 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
"instance_id": "ID of the Secrets Manager instance.",
"project_id": "STACKIT project ID to which the instance is associated.",
"name": "Instance name.",
"acls": "The access control list for this instance. Each entry is an IP or IP range that is permitted to access, in CIDR notation",
}

resp.Schema = schema.Schema{
Expand Down Expand Up @@ -138,6 +143,17 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r
stringvalidator.LengthAtLeast(1),
},
},
"acls": schema.ListAttribute{
Description: descriptions["acls"],
ElementType: types.StringType,
Optional: true,
Validators: []validator.List{
listvalidator.UniqueValues(),
listvalidator.ValueStringsAre(
validate.CIDR(),
),
},
},
},
}
}
Expand All @@ -153,6 +169,15 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
projectId := model.ProjectId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)

var acls []string
if !(model.ACLs.IsNull() || model.ACLs.IsUnknown()) {
diags = model.ACLs.ElementsAs(ctx, &acls, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}

// Generate API request body from model
payload, err := toCreatePayload(&model)
if err != nil {
Expand All @@ -168,8 +193,20 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques
instanceId := *createResp.Id
ctx = tflog.SetField(ctx, "instance_id", instanceId)

// Create ACLs
err = updateACLs(ctx, projectId, instanceId, acls, r.client)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating ACLs: %v", err))
return
}
aclList, err := r.client.GetAcls(ctx, projectId, instanceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API for ACLs data: %v", err))
return
}

// Map response body to schema
err = mapFields(createResp, &model)
err = mapFields(createResp, aclList, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Processing API payload: %v", err))
return
Expand Down Expand Up @@ -202,9 +239,14 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err))
return
}
aclList, err := r.client.GetAcls(ctx, projectId, instanceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API for ACLs data: %v", err))
return
}

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

// Update updates the resource and sets the updated Terraform state on success.
func (r *instanceResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// Update shouldn't be called
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", "Instance can't be updated")
func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
projectId := model.ProjectId.ValueString()
instanceId := model.InstanceId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
ctx = tflog.SetField(ctx, "instance_id", instanceId)

var acls []string
if !(model.ACLs.IsNull() || model.ACLs.IsUnknown()) {
diags = model.ACLs.ElementsAs(ctx, &acls, false)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}

// Update ACLs
err := updateACLs(ctx, projectId, instanceId, acls, r.client)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Updating ACLs: %v", err))
return
}

instanceResp, err := r.client.GetInstance(ctx, projectId, instanceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err))
return
}
aclList, err := r.client.GetAcls(ctx, projectId, instanceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API for ACLs data: %v", err))
return
}

// Map response body to schema
err = mapFields(instanceResp, aclList, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err))
return
}

diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Secrets Manager instance updated")
}

// Delete deletes the resource and removes the Terraform state on success.
Expand Down Expand Up @@ -266,7 +357,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS
tflog.Info(ctx, "Secrets Manager instance state imported")
}

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

err := mapACLs(aclList, model)
if err != nil {
return err
}

return nil
}

func mapACLs(aclList *secretsmanager.AclList, model *Model) error {
if aclList == nil {
return fmt.Errorf("nil ACL list")
}
if aclList.Acls == nil || len(*aclList.Acls) == 0 {
model.ACLs = types.ListNull(types.StringType)
return nil
}

acls := []attr.Value{}
for _, acl := range *aclList.Acls {
acls = append(acls, types.StringValue(*acl.Cidr))
}
aclsList, diags := types.ListValue(types.StringType, acls)
if diags.HasError() {
return fmt.Errorf("mapping ACLs: %w", core.DiagsToError(diags))
}
model.ACLs = aclsList
return nil
}

Expand All @@ -304,3 +421,54 @@ func toCreatePayload(model *Model) (*secretsmanager.CreateInstancePayload, error
Name: model.Name.ValueStringPointer(),
}, nil
}

// updateACLs creates and deletes ACLs so that the instance's ACLs are the ones in the model
func updateACLs(ctx context.Context, projectId, instanceId string, acls []string, client *secretsmanager.APIClient) error {
// Get ACLs current state
currentACLsResp, err := client.GetAcls(ctx, projectId, instanceId).Execute()
if err != nil {
return fmt.Errorf("fetching current ACLs: %w", err)
}

type aclState struct {
isInModel bool
isCreated bool
id string
}
aclsState := make(map[string]*aclState)
for _, cidr := range acls {
aclsState[cidr] = &aclState{
isInModel: true,
}
}
for _, acl := range *currentACLsResp.Acls {
cidr := *acl.Cidr
if _, ok := aclsState[cidr]; !ok {
aclsState[cidr] = &aclState{}
}
aclsState[cidr].isCreated = true
aclsState[cidr].id = *acl.Id
}

// Create/delete ACLs
for cidr, state := range aclsState {
if state.isInModel && !state.isCreated {
payload := secretsmanager.CreateAclPayload{
Cidr: utils.Ptr(cidr),
}
_, err := client.CreateAcl(ctx, projectId, instanceId).CreateAclPayload(payload).Execute()
if err != nil {
return fmt.Errorf("creating ACL '%v': %w", cidr, err)
}
}

if !state.isInModel && state.isCreated {
err := client.DeleteAcl(ctx, projectId, instanceId, state.id).Execute()
if err != nil {
return fmt.Errorf("deleting ACL '%v': %w", cidr, err)
}
}
}

return nil
}
Loading