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 <henrique.santos@freiheit.com>
This commit is contained in:
parent
3c6748545d
commit
e1265578ce
10 changed files with 770 additions and 13 deletions
27
docs/data-sources/secretsmanager_instance.md
Normal file
27
docs/data-sources/secretsmanager_instance.md
Normal file
|
|
@ -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
docs/resources/secretsmanager_instance.md
Normal file
30
docs/resources/secretsmanager_instance.md
Normal file
|
|
@ -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
go.mod
1
go.mod
|
|
@ -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
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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{
|
||||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
@ -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{
|
||||
|
|
@ -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(),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,20 @@
|
|||
package secretsmanager
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"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"
|
||||
)
|
||||
|
|
@ -13,17 +23,20 @@ func TestMapFields(t *testing.T) {
|
|||
tests := []struct {
|
||||
description string
|
||||
input *secretsmanager.Instance
|
||||
aclList *secretsmanager.AclList
|
||||
expected Model
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_values",
|
||||
&secretsmanager.Instance{},
|
||||
&secretsmanager.AclList{},
|
||||
Model{
|
||||
Id: types.StringValue("pid,iid"),
|
||||
InstanceId: types.StringValue("iid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
Name: types.StringNull(),
|
||||
ACLs: types.ListNull(types.StringType),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -32,23 +45,53 @@ func TestMapFields(t *testing.T) {
|
|||
&secretsmanager.Instance{
|
||||
Name: utils.Ptr("name"),
|
||||
},
|
||||
&secretsmanager.AclList{
|
||||
Acls: &[]secretsmanager.Acl{
|
||||
{
|
||||
Cidr: utils.Ptr("cidr-1"),
|
||||
Id: utils.Ptr("id-cidr-1"),
|
||||
},
|
||||
{
|
||||
Cidr: utils.Ptr("cidr-2"),
|
||||
Id: utils.Ptr("id-cidr-2"),
|
||||
},
|
||||
{
|
||||
Cidr: utils.Ptr("cidr-3"),
|
||||
Id: utils.Ptr("id-cidr-3"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,iid"),
|
||||
InstanceId: types.StringValue("iid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
Name: types.StringValue("name"),
|
||||
ACLs: types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("cidr-1"),
|
||||
types.StringValue("cidr-2"),
|
||||
types.StringValue("cidr-3"),
|
||||
}),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"nil_response",
|
||||
nil,
|
||||
&secretsmanager.AclList{},
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"nil_acli_list",
|
||||
&secretsmanager.Instance{},
|
||||
nil,
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no_resource_id",
|
||||
&secretsmanager.Instance{},
|
||||
&secretsmanager.AclList{},
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
|
|
@ -59,7 +102,7 @@ func TestMapFields(t *testing.T) {
|
|||
ProjectId: tt.expected.ProjectId,
|
||||
InstanceId: tt.expected.InstanceId,
|
||||
}
|
||||
err := mapFields(tt.input, state)
|
||||
err := mapFields(tt.input, tt.aclList, state)
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
@ -134,3 +177,313 @@ func TestToCreatePayload(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateACLs(t *testing.T) {
|
||||
// This is the response used when getting all ACLs currently, across all tests
|
||||
getAllACLsResp := secretsmanager.AclList{
|
||||
Acls: &[]secretsmanager.Acl{
|
||||
{
|
||||
Cidr: utils.Ptr("acl-1"),
|
||||
Id: utils.Ptr("id-acl-1"),
|
||||
},
|
||||
{
|
||||
Cidr: utils.Ptr("acl-2"),
|
||||
Id: utils.Ptr("id-acl-2"),
|
||||
},
|
||||
{
|
||||
Cidr: utils.Ptr("acl-3"),
|
||||
Id: utils.Ptr("id-acl-3"),
|
||||
},
|
||||
{
|
||||
Cidr: utils.Ptr("acl-2"),
|
||||
Id: utils.Ptr("id-acl-2-repeated"),
|
||||
},
|
||||
},
|
||||
}
|
||||
getAllACLsRespBytes, err := json.Marshal(getAllACLsResp)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal get all ACLs response: %v", err)
|
||||
}
|
||||
|
||||
// This is the response used whenever an API returns a failure response
|
||||
failureRespBytes := []byte("{\"message\": \"Something bad happened\"")
|
||||
|
||||
tests := []struct {
|
||||
description string
|
||||
acls []string
|
||||
getAllACLsFails bool
|
||||
createACLFails bool
|
||||
deleteACLFails bool
|
||||
isValid bool
|
||||
expectedACLsStates map[string]bool // Keys are CIDR; value is true if CIDR should exist at the end, false if should be deleted
|
||||
}{
|
||||
{
|
||||
description: "no_changes",
|
||||
acls: []string{"acl-3", "acl-2", "acl-1"},
|
||||
expectedACLsStates: map[string]bool{
|
||||
"acl-1": true,
|
||||
"acl-2": true,
|
||||
"acl-3": true,
|
||||
},
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
description: "create_acl",
|
||||
acls: []string{"acl-1", "acl-2", "acl-3", "acl-4"},
|
||||
expectedACLsStates: map[string]bool{
|
||||
"acl-1": true,
|
||||
"acl-2": true,
|
||||
"acl-3": true,
|
||||
"acl-4": true,
|
||||
},
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
description: "delete_acl",
|
||||
acls: []string{"acl-3", "acl-1"},
|
||||
expectedACLsStates: map[string]bool{
|
||||
"acl-1": true,
|
||||
"acl-2": false,
|
||||
"acl-3": true,
|
||||
},
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
description: "multiple_changes",
|
||||
acls: []string{"acl-4", "acl-3", "acl-1", "acl-5"},
|
||||
expectedACLsStates: map[string]bool{
|
||||
"acl-1": true,
|
||||
"acl-2": false,
|
||||
"acl-3": true,
|
||||
"acl-4": true,
|
||||
"acl-5": true,
|
||||
},
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
description: "multiple_changes_repetition",
|
||||
acls: []string{"acl-4", "acl-3", "acl-1", "acl-5", "acl-5"},
|
||||
expectedACLsStates: map[string]bool{
|
||||
"acl-1": true,
|
||||
"acl-2": false,
|
||||
"acl-3": true,
|
||||
"acl-4": true,
|
||||
"acl-5": true,
|
||||
},
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
description: "multiple_changes_2",
|
||||
acls: []string{"acl-4", "acl-5"},
|
||||
expectedACLsStates: map[string]bool{
|
||||
"acl-1": false,
|
||||
"acl-2": false,
|
||||
"acl-3": false,
|
||||
"acl-4": true,
|
||||
"acl-5": true,
|
||||
},
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
description: "multiple_changes_3",
|
||||
acls: []string{},
|
||||
expectedACLsStates: map[string]bool{
|
||||
"acl-1": false,
|
||||
"acl-2": false,
|
||||
"acl-3": false,
|
||||
},
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
description: "get_fails",
|
||||
acls: []string{"acl-1", "acl-2", "acl-3"},
|
||||
getAllACLsFails: true,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
description: "create_fails_1",
|
||||
acls: []string{"acl-1", "acl-2", "acl-3", "acl-4"},
|
||||
createACLFails: true,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
description: "create_fails_2",
|
||||
acls: []string{"acl-1", "acl-2"},
|
||||
createACLFails: true,
|
||||
expectedACLsStates: map[string]bool{
|
||||
"acl-1": true,
|
||||
"acl-2": true,
|
||||
"acl-3": false,
|
||||
},
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
description: "delete_fails_1",
|
||||
acls: []string{"acl-1", "acl-2"},
|
||||
deleteACLFails: true,
|
||||
isValid: false,
|
||||
},
|
||||
{
|
||||
description: "delete_fails_2",
|
||||
acls: []string{"acl-1", "acl-2", "acl-3", "acl-4"},
|
||||
deleteACLFails: true,
|
||||
expectedACLsStates: map[string]bool{
|
||||
"acl-1": true,
|
||||
"acl-2": true,
|
||||
"acl-3": true,
|
||||
"acl-4": true,
|
||||
},
|
||||
isValid: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
// Will be compared to tt.expectedACLsStates at the end
|
||||
aclsStates := make(map[string]bool)
|
||||
aclsStates["acl-1"] = true
|
||||
aclsStates["acl-2"] = true
|
||||
aclsStates["acl-3"] = true
|
||||
|
||||
// Handler for getting all ACLs
|
||||
getAllACLsHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if tt.getAllACLsFails {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, err := w.Write(failureRespBytes)
|
||||
if err != nil {
|
||||
t.Errorf("Get all ACLs handler: failed to write bad response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_, err := w.Write(getAllACLsRespBytes)
|
||||
if err != nil {
|
||||
t.Errorf("Get all ACLs handler: failed to write response: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
// Handler for creating ACL
|
||||
createACLHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var payload secretsmanager.CreateAclPayload
|
||||
err := decoder.Decode(&payload)
|
||||
if err != nil {
|
||||
t.Errorf("Create ACL handler: failed to parse payload")
|
||||
return
|
||||
}
|
||||
if payload.Cidr == nil {
|
||||
t.Errorf("Create ACL handler: nil CIDR")
|
||||
return
|
||||
}
|
||||
cidr := *payload.Cidr
|
||||
if cidrExists, cidrWasCreated := aclsStates[cidr]; cidrWasCreated && cidrExists {
|
||||
t.Errorf("Create ACL handler: attempted to create CIDR '%v' that already exists", *payload.Cidr)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if tt.createACLFails {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, err := w.Write(failureRespBytes)
|
||||
if err != nil {
|
||||
t.Errorf("Create ACL handler: failed to write bad response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp := secretsmanager.Acl{
|
||||
Cidr: utils.Ptr(cidr),
|
||||
Id: utils.Ptr(fmt.Sprintf("id-%s", cidr)),
|
||||
}
|
||||
respBytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
t.Errorf("Create ACL handler: failed to marshal response: %v", err)
|
||||
return
|
||||
}
|
||||
_, err = w.Write(respBytes)
|
||||
if err != nil {
|
||||
t.Errorf("Create ACL handler: failed to write response: %v", err)
|
||||
}
|
||||
aclsStates[cidr] = true
|
||||
})
|
||||
|
||||
// Handler for deleting ACL
|
||||
deleteACLHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
aclId, ok := vars["aclId"]
|
||||
if !ok {
|
||||
t.Errorf("Delete ACL handler: no ACL ID")
|
||||
return
|
||||
}
|
||||
cidr, ok := strings.CutPrefix(aclId, "id-")
|
||||
if !ok {
|
||||
t.Errorf("Delete ACL handler: got unexpected ACL ID '%v'", aclId)
|
||||
return
|
||||
}
|
||||
cidr, _ = strings.CutSuffix(cidr, "-repeated")
|
||||
cidrExists, cidrWasCreated := aclsStates[cidr]
|
||||
if !cidrWasCreated {
|
||||
t.Errorf("Delete ACL handler: attempted to delete CIDR '%v' that wasn't created", cidr)
|
||||
return
|
||||
}
|
||||
if cidrWasCreated && !cidrExists {
|
||||
t.Errorf("Delete ACL handler: attempted to delete CIDR '%v' that was already deleted", cidr)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if tt.deleteACLFails {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, err := w.Write(failureRespBytes)
|
||||
if err != nil {
|
||||
t.Errorf("Delete ACL handler: failed to write bad response: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte("{}"))
|
||||
if err != nil {
|
||||
t.Errorf("Delete ACL handler: failed to write response: %v", err)
|
||||
}
|
||||
aclsStates[cidr] = false
|
||||
})
|
||||
|
||||
// Setup server and client
|
||||
router := mux.NewRouter()
|
||||
router.HandleFunc("/v1/projects/{projectId}/instances/{instanceId}/acls", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "GET" {
|
||||
getAllACLsHandler(w, r)
|
||||
} else if r.Method == "POST" {
|
||||
createACLHandler(w, r)
|
||||
}
|
||||
})
|
||||
router.HandleFunc("/v1/projects/{projectId}/instances/{instanceId}/acls/{aclId}", deleteACLHandler)
|
||||
mockedServer := httptest.NewServer(router)
|
||||
defer mockedServer.Close()
|
||||
client, err := secretsmanager.NewAPIClient(
|
||||
config.WithEndpoint(mockedServer.URL),
|
||||
config.WithoutAuthentication(),
|
||||
config.WithRetryTimeout(time.Millisecond),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize client: %v", err)
|
||||
}
|
||||
|
||||
// Run test
|
||||
err = updateACLs(context.Background(), "pid", "iid", tt.acls, client)
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
if tt.isValid && err != nil {
|
||||
t.Fatalf("Should not have failed: %v", err)
|
||||
}
|
||||
if tt.isValid {
|
||||
diff := cmp.Diff(aclsStates, tt.expectedACLsStates)
|
||||
if diff != "" {
|
||||
t.Fatalf("ACL states do not match: %s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,24 +18,45 @@ import (
|
|||
|
||||
// Instance resource data
|
||||
var instanceResource = map[string]string{
|
||||
"project_id": testutil.ProjectId,
|
||||
"name": fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)),
|
||||
"project_id": testutil.ProjectId,
|
||||
"name": fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)),
|
||||
"acl-0": "1.2.3.4/5",
|
||||
"acl-1": "111.222.111.222/11",
|
||||
"acl-1-updated": "111.222.111.222/22",
|
||||
}
|
||||
|
||||
func resourceConfig() string {
|
||||
func resourceConfig(acls *string) string {
|
||||
if acls == nil {
|
||||
return fmt.Sprintf(`
|
||||
%s
|
||||
|
||||
resource "stackit_secretsmanager_instance" "instance" {
|
||||
project_id = "%s"
|
||||
name = "%s"
|
||||
}
|
||||
`,
|
||||
testutil.SecretsManagerProviderConfig(),
|
||||
instanceResource["project_id"],
|
||||
instanceResource["name"],
|
||||
)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`
|
||||
%s
|
||||
|
||||
resource "stackit_secretsmanager_instance" "instance" {
|
||||
project_id = "%s"
|
||||
name = "%s"
|
||||
acls = %s
|
||||
}
|
||||
`,
|
||||
testutil.SecretsManagerProviderConfig(),
|
||||
instanceResource["project_id"],
|
||||
instanceResource["name"],
|
||||
*acls,
|
||||
)
|
||||
}
|
||||
|
||||
func TestAccSecretsManager(t *testing.T) {
|
||||
resource.Test(t, resource.TestCase{
|
||||
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
|
||||
|
|
@ -44,12 +65,19 @@ func TestAccSecretsManager(t *testing.T) {
|
|||
|
||||
// Creation
|
||||
{
|
||||
Config: resourceConfig(),
|
||||
Config: resourceConfig(utils.Ptr(fmt.Sprintf(
|
||||
"[%q, %q]",
|
||||
instanceResource["acl-0"],
|
||||
instanceResource["acl-1"],
|
||||
))),
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
// Instance data
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", instanceResource["project_id"]),
|
||||
resource.TestCheckResourceAttrSet("stackit_secretsmanager_instance.instance", "instance_id"),
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "name", instanceResource["name"]),
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.#", "2"),
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.0", instanceResource["acl-0"]),
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.1", instanceResource["acl-1"]),
|
||||
),
|
||||
},
|
||||
{ // Data source
|
||||
|
|
@ -60,7 +88,11 @@ func TestAccSecretsManager(t *testing.T) {
|
|||
project_id = stackit_secretsmanager_instance.instance.project_id
|
||||
instance_id = stackit_secretsmanager_instance.instance.instance_id
|
||||
}`,
|
||||
resourceConfig(),
|
||||
resourceConfig(utils.Ptr(fmt.Sprintf(
|
||||
"[%q, %q]",
|
||||
instanceResource["acl-0"],
|
||||
instanceResource["acl-1"],
|
||||
))),
|
||||
),
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
// Instance data
|
||||
|
|
@ -70,6 +102,8 @@ func TestAccSecretsManager(t *testing.T) {
|
|||
"data.stackit_secretsmanager_instance.instance", "instance_id",
|
||||
),
|
||||
resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "name", instanceResource["name"]),
|
||||
resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "acls.0", instanceResource["acl-0"]),
|
||||
resource.TestCheckResourceAttr("data.stackit_secretsmanager_instance.instance", "acls.1", instanceResource["acl-1"]),
|
||||
),
|
||||
},
|
||||
// Import
|
||||
|
|
@ -89,6 +123,34 @@ func TestAccSecretsManager(t *testing.T) {
|
|||
ImportState: true,
|
||||
ImportStateVerify: true,
|
||||
},
|
||||
// Update
|
||||
{
|
||||
Config: resourceConfig(utils.Ptr(fmt.Sprintf(
|
||||
"[%q, %q]",
|
||||
instanceResource["acl-0"],
|
||||
instanceResource["acl-1-updated"],
|
||||
))),
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
// Instance data
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", instanceResource["project_id"]),
|
||||
resource.TestCheckResourceAttrSet("stackit_secretsmanager_instance.instance", "instance_id"),
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "name", instanceResource["name"]),
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.#", "2"),
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.0", instanceResource["acl-0"]),
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.1", instanceResource["acl-1-updated"]),
|
||||
),
|
||||
},
|
||||
// Update, no ACLs
|
||||
{
|
||||
Config: resourceConfig(nil),
|
||||
Check: resource.ComposeAggregateTestCheckFunc(
|
||||
// Instance data
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "project_id", instanceResource["project_id"]),
|
||||
resource.TestCheckResourceAttrSet("stackit_secretsmanager_instance.instance", "instance_id"),
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "name", instanceResource["name"]),
|
||||
resource.TestCheckResourceAttr("stackit_secretsmanager_instance.instance", "acls.#", "0"),
|
||||
),
|
||||
},
|
||||
// Deletion is done by the framework implicitly
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -137,3 +137,21 @@ func RFC3339SecondsOnly() *Validator {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
func CIDR() *Validator {
|
||||
description := "value must be in CIDR notation"
|
||||
|
||||
return &Validator{
|
||||
description: description,
|
||||
validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) {
|
||||
_, _, err := net.ParseCIDR(req.ConfigValue.ValueString())
|
||||
if err != nil {
|
||||
resp.Diagnostics.Append(validatordiag.InvalidAttributeValueDiagnostic(
|
||||
req.Path,
|
||||
fmt.Sprintf("parsing value in CIDR notation: %s", err.Error()),
|
||||
req.ConfigValue.ValueString(),
|
||||
))
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -262,3 +262,87 @@ func TestRFC3339SecondsOnly(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIDR(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
input string
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"IPv4_block",
|
||||
"198.51.100.14/24",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"IPv4_block_2",
|
||||
"111.222.111.222/22",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"IPv4_single",
|
||||
"198.51.100.14/32",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"IPv4_entire_internet",
|
||||
"0.0.0.0/0",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"IPv4_block_invalid",
|
||||
"198.51.100.14/33",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"IPv4_no_block",
|
||||
"111.222.111.222",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"IPv6_block",
|
||||
"2001:db8::/48",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"IPv6_single",
|
||||
"2001:0db8:85a3:08d3::0370:7344/128",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"IPv6_all",
|
||||
"::/0",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"IPv6_block_invalid",
|
||||
"2001:0db8:85a3:08d3::0370:7344/129",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"IPv6_no_block",
|
||||
"2001:0db8:85a3:08d3::0370:7344",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"empty",
|
||||
"",
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
r := validator.StringResponse{}
|
||||
CIDR().ValidateString(context.Background(), validator.StringRequest{
|
||||
ConfigValue: types.StringValue(tt.input),
|
||||
}, &r)
|
||||
|
||||
if !tt.isValid && !r.Diagnostics.HasError() {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
if tt.isValid && r.Diagnostics.HasError() {
|
||||
t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue