From a3e1e57cb2327adb7dffd1d3645a7becfa1b4ed7 Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Tue, 14 Apr 2026 13:11:47 +0200 Subject: [PATCH 1/4] feat(iam, secretsmanager): add secretsmanager IAM rolebinding resources relates to STACKITTPR-497 --- ...secretsmanager_instance_role_binding_v1.md | 33 ++ ...etsmanager_secret_group_role_binding_v1.md | 33 ++ scripts/project.sh | 2 +- .../iam/rolebindings/v1/generic/resource.go | 332 ++++++++++++++++++ .../rolebindings/v1/generic/resource_test.go | 72 ++++ .../rolebindings-testing/acc_test_builder.go | 121 +++++++ .../iam/rolebindings/v1/rolebindings.go | 16 + ...am_rolebindings_secretsmanager_acc_test.go | 46 +++ .../v1/services/secretsmanager/instance.go | 52 +++ .../services/secretsmanager/secret_group.go | 52 +++ .../secretsmanager/testdata/instance.tf | 15 + .../secretsmanager/secretsmanager_acc_test.go | 47 +-- .../services/secretsmanager/utils/util.go | 20 ++ .../secretsmanager/utils/util_test.go | 76 ++++ stackit/internal/testdestroy/acc_destroy.go | 21 ++ .../internal/testdestroy/secretsmanager.go | 51 +++ stackit/provider.go | 2 + templates/resources.md.tmpl | 87 +++++ 18 files changed, 1033 insertions(+), 45 deletions(-) create mode 100644 docs/resources/secretsmanager_instance_role_binding_v1.md create mode 100644 docs/resources/secretsmanager_secret_group_role_binding_v1.md create mode 100644 stackit/internal/services/iam/rolebindings/v1/generic/resource.go create mode 100644 stackit/internal/services/iam/rolebindings/v1/generic/resource_test.go create mode 100644 stackit/internal/services/iam/rolebindings/v1/rolebindings-testing/acc_test_builder.go create mode 100644 stackit/internal/services/iam/rolebindings/v1/rolebindings.go create mode 100644 stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/iam_rolebindings_secretsmanager_acc_test.go create mode 100644 stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/instance.go create mode 100644 stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/secret_group.go create mode 100644 stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/testdata/instance.tf create mode 100644 stackit/internal/testdestroy/acc_destroy.go create mode 100644 stackit/internal/testdestroy/secretsmanager.go create mode 100644 templates/resources.md.tmpl diff --git a/docs/resources/secretsmanager_instance_role_binding_v1.md b/docs/resources/secretsmanager_instance_role_binding_v1.md new file mode 100644 index 000000000..a93536fbe --- /dev/null +++ b/docs/resources/secretsmanager_instance_role_binding_v1.md @@ -0,0 +1,33 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_secretsmanager_instance_role_binding_v1 Resource - stackit" +subcategory: "" +description: |- + IAM role binding resource schema. + ~> This resource is part of the iam experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. +--- + +# stackit_secretsmanager_instance_role_binding_v1 (Resource) + +IAM role binding resource schema. + +~> This resource is part of the iam experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. + + + + +## Schema + +### Required + +- `resource_id` (String) The identifier of the resource to apply this role binding to. +- `role` (String) A valid role defined for the resource. +- `subject` (String) Identifier of user, service account or client. Usually email address or name in case of clients. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource identifier. It is structured as "`region`,`resource_id`,`role`,`subject`". diff --git a/docs/resources/secretsmanager_secret_group_role_binding_v1.md b/docs/resources/secretsmanager_secret_group_role_binding_v1.md new file mode 100644 index 000000000..32cfa5603 --- /dev/null +++ b/docs/resources/secretsmanager_secret_group_role_binding_v1.md @@ -0,0 +1,33 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_secretsmanager_secret_group_role_binding_v1 Resource - stackit" +subcategory: "" +description: |- + IAM role binding resource schema. + ~> This resource is part of the iam experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. +--- + +# stackit_secretsmanager_secret_group_role_binding_v1 (Resource) + +IAM role binding resource schema. + +~> This resource is part of the iam experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. + + + + +## Schema + +### Required + +- `resource_id` (String) The identifier of the resource to apply this role binding to. +- `role` (String) A valid role defined for the resource. +- `subject` (String) Identifier of user, service account or client. Usually email address or name in case of clients. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal resource identifier. It is structured as "`region`,`resource_id`,`role`,`subject`". diff --git a/scripts/project.sh b/scripts/project.sh index 48dc586f2..900e2eb12 100755 --- a/scripts/project.sh +++ b/scripts/project.sh @@ -16,7 +16,7 @@ elif [ "$action" = "tools" ]; then go mod download - go install github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs@v0.21.0 + go install github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs@v0.24.0 else echo "Invalid action: '$action', please use $0 help for help" fi diff --git a/stackit/internal/services/iam/rolebindings/v1/generic/resource.go b/stackit/internal/services/iam/rolebindings/v1/generic/resource.go new file mode 100644 index 000000000..693e92287 --- /dev/null +++ b/stackit/internal/services/iam/rolebindings/v1/generic/resource.go @@ -0,0 +1,332 @@ +package generic + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + secretsmanagerV1Alpha "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1alphaapi" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &RoleBindingResource[secretsmanagerV1Alpha.APIClient]{} + _ resource.ResourceWithConfigure = &RoleBindingResource[secretsmanagerV1Alpha.APIClient]{} + _ resource.ResourceWithImportState = &RoleBindingResource[secretsmanagerV1Alpha.APIClient]{} +) + +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + Region types.String `tfsdk:"region"` + ResourceId types.String `tfsdk:"resource_id"` + Role types.String `tfsdk:"role"` + Subject types.String `tfsdk:"subject"` +} + +type GenericRoleBindingResponse interface { + GetRole() string + GetSubject() string +} + +// RoleBindingResource is the resource implementation. +type RoleBindingResource[C any] struct { + providerData core.ProviderData + apiClient *C + + ApiName string // e.g. "iaas", "secretsmanager", ... + ResourceType string // e.g. "instance", ... + + // callbacks for lifecyle handling + ApiClientFactory func(context.Context, *core.ProviderData, *diag.Diagnostics) *C + ExecReadRequest func(ctx context.Context, client *C, region, resourceId, role, subject string) (GenericRoleBindingResponse, error) + ExecCreateRequest func(ctx context.Context, client *C, region, resourceId, role, subject string) (GenericRoleBindingResponse, error) + ExecUpdateRequest func(ctx context.Context, client *C, region, resourceId, role, subject string) (GenericRoleBindingResponse, error) + ExecDeleteRequest func(ctx context.Context, client *C, region, resourceId, role, subject string) error +} + +// Metadata returns the resource type name. +func (r *RoleBindingResource[C]) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_%s_%s_role_binding_v1", req.ProviderTypeName, r.ApiName, r.ResourceType) +} + +// Configure adds the provider configured client to the resource. +func (r *RoleBindingResource[C]) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckExperimentEnabled(ctx, &providerData, features.IamExperiment, fmt.Sprintf("stackit_%s_%s_role_binding_v1", r.ApiName, r.ResourceType), core.Resource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + r.apiClient = r.ApiClientFactory(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, fmt.Sprintf("%s %s client configured", r.ApiName, r.ResourceType)) +} + +// Schema defines the schema for the resource. +func (r *RoleBindingResource[C]) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: features.AddExperimentDescription("IAM role binding resource schema.", features.IamExperiment, core.Resource), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal resource identifier. It is structured as \"`region`,`resource_id`,`role`,`subject`\".", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "resource_id": schema.StringAttribute{ + Description: "The identifier of the resource to apply this role binding to.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "role": schema.StringAttribute{ + Description: "A valid role defined for the resource.", + Required: true, + }, + "subject": schema.StringAttribute{ + Description: "Identifier of user, service account or client. Usually email address or name in case of clients.", + Required: true, + }, + "region": schema.StringAttribute{ + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + Description: "The resource region. If not defined, the provider region is used.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *RoleBindingResource[C]) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // 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 + } + + ctx = core.InitProviderContext(ctx) + + region := r.providerData.GetRegionWithOverride(model.Region) + resourceId := model.ResourceId.ValueString() + + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "resource_id", resourceId) + + // TODO: remove + time.Sleep(10 * time.Second) + + roleBindingResp, err := r.ExecCreateRequest(ctx, r.apiClient, region, resourceId, model.Role.ValueString(), model.Subject.ValueString()) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error creating %s %s role binding", r.ApiName, r.ResourceType), fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(roleBindingResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error creating %s %s role binding", r.ApiName, r.ResourceType), fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + select { + case <-ctx.Done(): + return + case <-time.After(10 * time.Second): // safety sleep due to api cache + // continue + } + + tflog.Info(ctx, fmt.Sprintf("%s %s role binding created", r.ApiName, r.ResourceType)) +} + +// Read refreshes the Terraform state with the latest data. +func (r *RoleBindingResource[C]) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + region := r.providerData.GetRegionWithOverride(model.Region) + resourceId := model.ResourceId.ValueString() + + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "resource_id", resourceId) + + roleBindingResp, err := r.ExecReadRequest(ctx, r.apiClient, region, resourceId, model.Role.ValueString(), model.Subject.ValueString()) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error reading %s %s role binding", r.ApiName, r.ResourceType), fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + // Map response body to schema + err = mapFields(roleBindingResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error reading %s %s role binding", r.ApiName, r.ResourceType), fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, fmt.Sprintf("%s %s role binding read", r.ApiName, r.ResourceType)) +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *RoleBindingResource[C]) 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 + } + + ctx = core.InitProviderContext(ctx) + + region := r.providerData.GetRegionWithOverride(model.Region) + resourceId := model.ResourceId.ValueString() + + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "resource_id", resourceId) + + roleBindingResp, err := r.ExecUpdateRequest(ctx, r.apiClient, region, resourceId, model.Role.ValueString(), model.Subject.ValueString()) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error updating %s %s role binding", r.ApiName, r.ResourceType), fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + err = mapFields(roleBindingResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error updating %s %s role binding", r.ApiName, r.ResourceType), fmt.Sprintf("Processing API payload: %v", err)) + return + } + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + select { + case <-ctx.Done(): + return + case <-time.After(10 * time.Second): // safety sleep due to api cache + // continue + } + + tflog.Info(ctx, fmt.Sprintf("%s %s role binding updated", r.ApiName, r.ResourceType)) +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *RoleBindingResource[C]) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + ctx = core.InitProviderContext(ctx) + + region := r.providerData.GetRegionWithOverride(model.Region) + resourceId := model.ResourceId.ValueString() + + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "resource_id", resourceId) + + err := r.ExecDeleteRequest(ctx, r.apiClient, region, resourceId, model.Role.ValueString(), model.Subject.ValueString()) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error deleting %s %s role binding", r.ApiName, r.ResourceType), fmt.Sprintf("Calling API: %v", err)) + } + + select { + case <-ctx.Done(): + return + case <-time.After(10 * time.Second): // safety sleep due to api cache + // continue + } + + ctx = core.LogResponse(ctx) + tflog.Info(ctx, fmt.Sprintf("%s %s role binding deleted", r.ApiName, r.ResourceType)) +} + +// ImportState imports a resource into the Terraform state on success. +func (r *RoleBindingResource[C]) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + fmt.Sprintf("Error importing %s %s role binding", r.ApiName, r.ResourceType), + fmt.Sprintf("Expected import identifier with format [region],[resource_id],[role],[subject], got %q", req.ID), + ) + return + } + + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "region": idParts[0], + "resource_id": idParts[1], + "role": idParts[2], + "subject": idParts[3], + }) + + tflog.Info(ctx, fmt.Sprintf("%s %s role binding state imported", r.ApiName, r.ResourceType)) +} + +func mapFields(resp GenericRoleBindingResponse, model *Model, region string) error { + if resp == nil { + return fmt.Errorf("nil response") + } else if model == nil { + return fmt.Errorf("nil model") + } + + role := resp.GetRole() + subject := resp.GetSubject() + + model.Id = utils.BuildInternalTerraformId(region, model.ResourceId.ValueString(), role, subject) + model.Region = types.StringValue(region) + model.Role = types.StringValue(role) + model.Subject = types.StringValue(subject) + + return nil +} diff --git a/stackit/internal/services/iam/rolebindings/v1/generic/resource_test.go b/stackit/internal/services/iam/rolebindings/v1/generic/resource_test.go new file mode 100644 index 000000000..9936a0b5f --- /dev/null +++ b/stackit/internal/services/iam/rolebindings/v1/generic/resource_test.go @@ -0,0 +1,72 @@ +package generic + +import ( + "fmt" + "testing" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/types" + secretsmanagerV1Alpha "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1alphaapi" +) + +func Test_mapFields(t *testing.T) { + const testRegion = "eu01" + resourceId := uuid.New().String() + + type args struct { + resp GenericRoleBindingResponse + model *Model + region string + } + tests := []struct { + name string + args args + wantModel *Model + wantErr bool + }{ + { + name: "default", + args: args{ + region: testRegion, + resp: &secretsmanagerV1Alpha.RoleBinding{ + Role: "owner", + Subject: "john.doe@example.com", + }, + model: &Model{ + ResourceId: types.StringValue(resourceId), + }, + }, + wantModel: &Model{ + Id: types.StringValue(fmt.Sprintf("%s,%s,owner,john.doe@example.com", testRegion, resourceId)), + ResourceId: types.StringValue(resourceId), + Role: types.StringValue("owner"), + Subject: types.StringValue("john.doe@example.com"), + Region: types.StringValue(testRegion), + }, + wantErr: false, + }, + { + name: "model is nil", + args: args{ + resp: &secretsmanagerV1Alpha.RoleBinding{}, + model: nil, + }, + wantErr: true, + }, + { + name: "response is nil", + args: args{ + resp: nil, + model: &Model{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := mapFields(tt.args.resp, tt.args.model, tt.args.region); (err != nil) != tt.wantErr { + t.Errorf("mapFields() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/stackit/internal/services/iam/rolebindings/v1/rolebindings-testing/acc_test_builder.go b/stackit/internal/services/iam/rolebindings/v1/rolebindings-testing/acc_test_builder.go new file mode 100644 index 000000000..a6d9540fa --- /dev/null +++ b/stackit/internal/services/iam/rolebindings/v1/rolebindings-testing/acc_test_builder.go @@ -0,0 +1,121 @@ +package rolebindings_testing + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testdestroy" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +func NewRoleBindingAccTestBuilder(tfProviderConfig, apiName, resourceType, resourceID string) *RoleBindingAccTestBuilder { + return &RoleBindingAccTestBuilder{ + providerConfig: tfProviderConfig, + resourceIdentifier: "stackit_" + apiName + "_" + resourceType + "_role_binding_v1." + resourceID, + } +} + +// RoleBindingAccTestBuilder helps to implement acceptance tests for role binding resources and is used to prevent the boilerplate code needed for that type of tests. +type RoleBindingAccTestBuilder struct { + providerConfig string + + resourceIdentifier string // e.g. "stackit_secretsmanager_instance_role_binding.role_binding" + + // Note: Keep these steps here in the order they are executed later + createStep *resource.TestStep + importStep *resource.TestStep + updateStep *resource.TestStep +} + +// CreateStep is the first step in your acceptance test and creates the resources initially +func (b *RoleBindingAccTestBuilder) CreateStep(tfConfig string, variables config.Variables, resourceIdResourceID, resourceIdField string) *RoleBindingAccTestBuilder { + b.createStep = &resource.TestStep{ + Config: b.providerConfig + "\n" + tfConfig, + ConfigVariables: variables, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair( + b.resourceIdentifier, "resource_id", + resourceIdResourceID, resourceIdField, + ), + resource.TestCheckResourceAttr(b.resourceIdentifier, "role", testutil.ConvertConfigVariable(variables["role"])), + resource.TestCheckResourceAttr(b.resourceIdentifier, "subject", testutil.ConvertConfigVariable(variables["subject"])), + ), + } + return b +} + +// ImportStep adds a terraform import test to your acceptance test case +func (b *RoleBindingAccTestBuilder) ImportStep(variables config.Variables) *RoleBindingAccTestBuilder { + b.importStep = &resource.TestStep{ + ConfigVariables: variables, + ResourceName: b.resourceIdentifier, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources[b.resourceIdentifier] + if !ok { + return "", fmt.Errorf("couldn't find resource %s", b.resourceIdentifier) + } + + resourceId, ok := r.Primary.Attributes["resource_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute resource_id") + } + + subject, ok := r.Primary.Attributes["subject"] + if !ok { + return "", fmt.Errorf("couldn't find attribute subject") + } + + role, ok := r.Primary.Attributes["role"] + if !ok { + return "", fmt.Errorf("couldn't find attribute role") + } + + return fmt.Sprintf("%s,%s,%s,%s", testutil.Region, resourceId, role, subject), nil + }, + ImportState: true, + ImportStateVerify: true, + } + return b +} + +// UpdateStep is the first step in your acceptance test and updates the resources +func (b *RoleBindingAccTestBuilder) UpdateStep(tfConfig string, variables config.Variables, resourceIdResourceID, resourceIdField string) *RoleBindingAccTestBuilder { + b.createStep = &resource.TestStep{ + Config: b.providerConfig + "\n" + tfConfig, + ConfigVariables: variables, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair( + b.resourceIdentifier, "resource_id", + resourceIdResourceID, resourceIdField, + ), + resource.TestCheckResourceAttr(b.resourceIdentifier, "role", testutil.ConvertConfigVariable(variables["role"])), + resource.TestCheckResourceAttr(b.resourceIdentifier, "subject", testutil.ConvertConfigVariable(variables["subject"])), + ), + } + return b +} + +func (b *RoleBindingAccTestBuilder) Build() resource.TestCase { + tc := resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testdestroy.AccTestCheckDestroy, + Steps: []resource.TestStep{}, + } + + if b.createStep != nil { + tc.Steps = append(tc.Steps, *b.createStep) + } + + if b.importStep != nil { + tc.Steps = append(tc.Steps, *b.importStep) + } + + if b.updateStep != nil { + tc.Steps = append(tc.Steps, *b.updateStep) + } + + return tc +} diff --git a/stackit/internal/services/iam/rolebindings/v1/rolebindings.go b/stackit/internal/services/iam/rolebindings/v1/rolebindings.go new file mode 100644 index 000000000..2e1cf7a56 --- /dev/null +++ b/stackit/internal/services/iam/rolebindings/v1/rolebindings.go @@ -0,0 +1,16 @@ +package v1 + +import ( + "github.com/hashicorp/terraform-plugin-framework/resource" + + secretsmanager2 "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager" +) + +// NewRoleBindingResources is a helper function to simplify the provider implementation. +func NewRoleBindingResources() []func() resource.Resource { + return []func() resource.Resource{ + // secretsmanager + secretsmanager2.NewSecretsmanagerInstanceRoleBindingResource, + secretsmanager2.NewSecretsmanagerSecretGroupRoleBindingResource, + } +} diff --git a/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/iam_rolebindings_secretsmanager_acc_test.go b/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/iam_rolebindings_secretsmanager_acc_test.go new file mode 100644 index 000000000..3fd62873a --- /dev/null +++ b/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/iam_rolebindings_secretsmanager_acc_test.go @@ -0,0 +1,46 @@ +package secretsmanager_test + +import ( + _ "embed" + "maps" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + rolebindings_testing "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/rolebindings-testing" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +var ( + //go:embed testdata/instance.tf + instanceConfig string +) + +func TestAccSecretsManagerInstanceRoleBindings(t *testing.T) { + variables := config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + "instance_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)), + "role": config.StringVariable("owner"), + "subject": config.StringVariable(testutil.TestProjectServiceAccountEmail), + } + + variablesUpdated := func() config.Variables { + tempConfig := make(config.Variables, len(variables)) + maps.Copy(tempConfig, variables) + tempConfig["role"] = config.StringVariable("editor") + return tempConfig + } + + providerConfig := testutil.NewConfigBuilder().Experiments(testutil.ExperimentIAM).BuildProviderConfig() + + tc := rolebindings_testing.NewRoleBindingAccTestBuilder(providerConfig, "secretsmanager", "instance", "role_binding"). + CreateStep(instanceConfig, variables, "stackit_secretsmanager_instance.instance", "instance_id"). + ImportStep(variables). + UpdateStep(instanceConfig, variablesUpdated(), "stackit_secretsmanager_instance.instance", "instance_id"). + Build() + + resource.Test(t, tc) +} diff --git a/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/instance.go b/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/instance.go new file mode 100644 index 000000000..f9da68eed --- /dev/null +++ b/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/instance.go @@ -0,0 +1,52 @@ +package secretsmanager + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + secretsmanagerV1Alpha "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1alphaapi" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/generic" + + secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" +) + +func NewSecretsmanagerInstanceRoleBindingResource() resource.Resource { + return &generic.RoleBindingResource[secretsmanagerV1Alpha.APIClient]{ + ApiName: "secretsmanager", + ResourceType: "instance", + ApiClientFactory: secretsmanagerUtils.ConfigureV1AlphaClient, + ExecCreateRequest: func(ctx context.Context, client *secretsmanagerV1Alpha.APIClient, region, resourceId, role, subject string) (generic.GenericRoleBindingResponse, error) { + payload := secretsmanagerV1Alpha.AddInstanceRoleBindingsPayload{ + Role: role, + Subject: subject, + } + + return client.DefaultAPI.AddInstanceRoleBindings(ctx, region, resourceId).AddInstanceRoleBindingsPayload(payload).Execute() + }, + ExecReadRequest: func(ctx context.Context, client *secretsmanagerV1Alpha.APIClient, region, resourceId, role, subject string) (generic.GenericRoleBindingResponse, error) { + payload := secretsmanagerV1Alpha.GetInstanceRoleBindingsPayload{ + Role: role, + Subject: subject, + } + + return client.DefaultAPI.GetInstanceRoleBindings(ctx, region, resourceId).GetInstanceRoleBindingsPayload(payload).Execute() + }, + ExecUpdateRequest: func(ctx context.Context, client *secretsmanagerV1Alpha.APIClient, region, resourceId, role, subject string) (generic.GenericRoleBindingResponse, error) { + payload := secretsmanagerV1Alpha.EditInstanceRoleBindingsPayload{ + Role: role, + Subject: subject, + } + + return client.DefaultAPI.EditInstanceRoleBindings(ctx, region, resourceId).EditInstanceRoleBindingsPayload(payload).Execute() + }, + ExecDeleteRequest: func(ctx context.Context, client *secretsmanagerV1Alpha.APIClient, region, resourceId, role, subject string) error { + payload := secretsmanagerV1Alpha.RemoveInstanceRoleBindingsPayload{ + Role: role, + Subject: subject, + } + + return client.DefaultAPI.RemoveInstanceRoleBindings(ctx, region, resourceId).RemoveInstanceRoleBindingsPayload(payload).Execute() + }, + } +} diff --git a/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/secret_group.go b/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/secret_group.go new file mode 100644 index 000000000..2427ae340 --- /dev/null +++ b/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/secret_group.go @@ -0,0 +1,52 @@ +package secretsmanager + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" + secretsmanagerV1Alpha "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1alphaapi" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/generic" + + secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" +) + +func NewSecretsmanagerSecretGroupRoleBindingResource() resource.Resource { + return &generic.RoleBindingResource[secretsmanagerV1Alpha.APIClient]{ + ApiName: "secretsmanager", + ResourceType: "secret_group", + ApiClientFactory: secretsmanagerUtils.ConfigureV1AlphaClient, + ExecCreateRequest: func(ctx context.Context, client *secretsmanagerV1Alpha.APIClient, region, resourceId, role, subject string) (generic.GenericRoleBindingResponse, error) { + payload := secretsmanagerV1Alpha.AddSecretGroupRoleBindingsPayload{ + Role: role, + Subject: subject, + } + + return client.DefaultAPI.AddSecretGroupRoleBindings(ctx, region, resourceId).AddSecretGroupRoleBindingsPayload(payload).Execute() + }, + ExecReadRequest: func(ctx context.Context, client *secretsmanagerV1Alpha.APIClient, region, resourceId, role, subject string) (generic.GenericRoleBindingResponse, error) { + payload := secretsmanagerV1Alpha.GetSecretGroupRoleBindingsPayload{ + Role: role, + Subject: subject, + } + + return client.DefaultAPI.GetSecretGroupRoleBindings(ctx, region, resourceId).GetSecretGroupRoleBindingsPayload(payload).Execute() + }, + ExecUpdateRequest: func(ctx context.Context, client *secretsmanagerV1Alpha.APIClient, region, resourceId, role, subject string) (generic.GenericRoleBindingResponse, error) { + payload := secretsmanagerV1Alpha.EditSecretGroupRoleBindingsPayload{ + Role: role, + Subject: subject, + } + + return client.DefaultAPI.EditSecretGroupRoleBindings(ctx, region, resourceId).EditSecretGroupRoleBindingsPayload(payload).Execute() + }, + ExecDeleteRequest: func(ctx context.Context, client *secretsmanagerV1Alpha.APIClient, region, resourceId, role, subject string) error { + payload := secretsmanagerV1Alpha.RemoveSecretGroupRoleBindingsPayload{ + Role: role, + Subject: subject, + } + + return client.DefaultAPI.RemoveSecretGroupRoleBindings(ctx, region, resourceId).RemoveSecretGroupRoleBindingsPayload(payload).Execute() + }, + } +} diff --git a/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/testdata/instance.tf b/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/testdata/instance.tf new file mode 100644 index 000000000..57eb18a6c --- /dev/null +++ b/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/testdata/instance.tf @@ -0,0 +1,15 @@ +variable "project_id" {} +variable "instance_name" {} +variable "role" {} +variable "subject" {} + +resource "stackit_secretsmanager_instance" "instance" { + project_id = var.project_id + name = var.instance_name +} + +resource "stackit_secretsmanager_instance_role_binding_v1" "role_binding" { + resource_id = stackit_secretsmanager_instance.instance.instance_id + role = var.role + subject = var.subject +} \ No newline at end of file diff --git a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go index 8257265e4..0583be16a 100644 --- a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go +++ b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go @@ -1,12 +1,10 @@ package secretsmanager_test import ( - "context" _ "embed" "fmt" "maps" "regexp" - "strings" "testing" "github.com/hashicorp/terraform-plugin-testing/config" @@ -14,10 +12,8 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testdestroy" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) @@ -72,7 +68,7 @@ func configVarsMaxUpdated() config.Variables { func TestAccSecretsManagerMin(t *testing.T) { resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSecretsManagerDestroy, + CheckDestroy: testdestroy.AccTestCheckDestroy, Steps: []resource.TestStep{ // Creation fail { @@ -227,7 +223,7 @@ func TestAccSecretsManagerMin(t *testing.T) { func TestAccSecretsManagerMax(t *testing.T) { resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, - CheckDestroy: testAccCheckSecretsManagerDestroy, + CheckDestroy: testdestroy.AccTestCheckDestroy, Steps: []resource.TestStep{ // Creation fail { @@ -447,40 +443,3 @@ func TestAccSecretsManagerMax(t *testing.T) { }, }) } - -func testAccCheckSecretsManagerDestroy(s *terraform.State) error { - ctx := context.Background() - client, err := secretsmanager.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.SecretsManagerCustomEndpoint, true)...) - if err != nil { - return fmt.Errorf("creating client: %w", err) - } - - instancesToDestroy := []string{} - for _, rs := range s.RootModule().Resources { - if rs.Type != "stackit_secretsmanager_instance" { - continue - } - // instance terraform ID: "[project_id],[instance_id]" - instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] - instancesToDestroy = append(instancesToDestroy, instanceId) - } - - instancesResp, err := client.DefaultAPI.ListInstances(ctx, testutil.ProjectId).Execute() - if err != nil { - return fmt.Errorf("getting instancesResp: %w", err) - } - - instances := instancesResp.Instances - for i := range instances { - if instances[i].Id == "" { - continue - } - if utils.Contains(instancesToDestroy, instances[i].Id) { - err := client.DefaultAPI.DeleteInstance(ctx, testutil.ProjectId, instances[i].Id).Execute() - if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", instances[i].Id, err) - } - } - } - return nil -} diff --git a/stackit/internal/services/secretsmanager/utils/util.go b/stackit/internal/services/secretsmanager/utils/util.go index dd9d2fa96..8737fea36 100644 --- a/stackit/internal/services/secretsmanager/utils/util.go +++ b/stackit/internal/services/secretsmanager/utils/util.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" + secretsmanagerV1Alpha "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1alphaapi" secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -30,3 +31,22 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags return apiClient } + +func ConfigureV1AlphaClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *secretsmanagerV1Alpha.APIClient { + apiClientConfigOptions := []config.ConfigurationOption{ + config.WithCustomAuth(providerData.RoundTripper), + utils.UserAgentConfigOption(providerData.Version), + } + if providerData.SecretsManagerCustomEndpoint != "" { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.SecretsManagerCustomEndpoint)) + } else { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) + } + apiClient, err := secretsmanagerV1Alpha.NewAPIClient(apiClientConfigOptions...) + if err != nil { + core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) + return nil + } + + return apiClient +} diff --git a/stackit/internal/services/secretsmanager/utils/util_test.go b/stackit/internal/services/secretsmanager/utils/util_test.go index dc3866669..4c0006e4f 100644 --- a/stackit/internal/services/secretsmanager/utils/util_test.go +++ b/stackit/internal/services/secretsmanager/utils/util_test.go @@ -11,6 +11,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/config" secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" + secretsmanagerV1Alpha "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1alphaapi" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -93,3 +95,77 @@ func TestConfigureClient(t *testing.T) { }) } } + +func TestConfigureV1AlphaClient(t *testing.T) { + /* mock authentication by setting service account token env variable */ + os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") + if err != nil { + t.Errorf("error setting env variable: %v", err) + } + + type args struct { + providerData *core.ProviderData + } + tests := []struct { + name string + args args + wantErr bool + expected *secretsmanagerV1Alpha.APIClient + }{ + { + name: "default endpoint", + args: args{ + providerData: &core.ProviderData{ + Version: testVersion, + }, + }, + expected: func() *secretsmanagerV1Alpha.APIClient { + apiClient, err := secretsmanagerV1Alpha.NewAPIClient( + config.WithRegion("eu01"), + utils.UserAgentConfigOption(testVersion), + ) + if err != nil { + t.Errorf("error configuring client: %v", err) + } + return apiClient + }(), + wantErr: false, + }, + { + name: "custom endpoint", + args: args{ + providerData: &core.ProviderData{ + Version: testVersion, + SecretsManagerCustomEndpoint: testCustomEndpoint, + }, + }, + expected: func() *secretsmanagerV1Alpha.APIClient { + apiClient, err := secretsmanagerV1Alpha.NewAPIClient( + utils.UserAgentConfigOption(testVersion), + config.WithEndpoint(testCustomEndpoint), + ) + if err != nil { + t.Errorf("error configuring client: %v", err) + } + return apiClient + }(), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + diags := diag.Diagnostics{} + + actual := ConfigureV1AlphaClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { + t.Errorf("ConfigureV1AlphaClient() error = %v, want %v", diags.HasError(), tt.wantErr) + } + + if !reflect.DeepEqual(actual, tt.expected) { + t.Errorf("ConfigureV1AlphaClient() = %v, want %v", actual, tt.expected) + } + }) + } +} diff --git a/stackit/internal/testdestroy/acc_destroy.go b/stackit/internal/testdestroy/acc_destroy.go new file mode 100644 index 000000000..8ef983ffc --- /dev/null +++ b/stackit/internal/testdestroy/acc_destroy.go @@ -0,0 +1,21 @@ +package testdestroy + +import ( + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +// AccTestCheckDestroy is a helper function to destroy potential leftover resources from acceptance tests +func AccTestCheckDestroy(s *terraform.State) error { + destroyFuncs := []func(state *terraform.State) error{ + testAccCheckSecretsManagerDestroy, + } + + for _, fn := range destroyFuncs { + err := fn(s) + if err != nil { + return err + } + } + + return nil +} diff --git a/stackit/internal/testdestroy/secretsmanager.go b/stackit/internal/testdestroy/secretsmanager.go new file mode 100644 index 000000000..aebfafc04 --- /dev/null +++ b/stackit/internal/testdestroy/secretsmanager.go @@ -0,0 +1,51 @@ +package testdestroy + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +func testAccCheckSecretsManagerDestroy(s *terraform.State) error { + ctx := context.Background() + client, err := secretsmanager.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.SecretsManagerCustomEndpoint, true)...) + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_secretsmanager_instance" { + continue + } + // instance terraform ID: "[project_id],[instance_id]" + instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] + instancesToDestroy = append(instancesToDestroy, instanceId) + } + + instancesResp, err := client.ListInstances(ctx, testutil.ProjectId).Execute() + if err != nil { + return fmt.Errorf("getting instancesResp: %w", err) + } + + instances := *instancesResp.Instances + for i := range instances { + if instances[i].Id == nil { + continue + } + if utils.Contains(instancesToDestroy, *instances[i].Id) { + err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].Id) + if err != nil { + return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].Id, err) + } + } + } + return nil +} diff --git a/stackit/provider.go b/stackit/provider.go index f08e2e6af..3abeb1167 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -61,6 +61,7 @@ import ( iaasServiceAccountAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/serviceaccountattach" iaasVolume "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/volume" iaasVolumeAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/volumeattach" + iamRoleBindingsV1 "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1" kmsKey "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/key" kmsKeyRing "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/keyring" kmsWrappingKey "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/kms/wrapping-key" @@ -790,6 +791,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { } resources = append(resources, roleAssignements.NewRoleAssignmentResources()...) resources = append(resources, customRole.NewCustomRoleResources()...) + resources = append(resources, iamRoleBindingsV1.NewRoleBindingResources()...) return resources } diff --git a/templates/resources.md.tmpl b/templates/resources.md.tmpl new file mode 100644 index 000000000..91fc9707b --- /dev/null +++ b/templates/resources.md.tmpl @@ -0,0 +1,87 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "{{.Name}} {{.Type}} - {{.RenderedProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- +{{/* Check whether this is a iam role binding resource. The check looks cursed because there's no hasSuffix function available here */}} +{{- $isRoleBinding := false -}} + +{{- $parts := split .Name "_role_binding" -}} +{{- $lastItem := "" -}} + +{{- range $parts -}} + {{- $lastItem = . -}} +{{- end -}} + +{{- if and (gt (len $parts) 1) (eq $lastItem "") -}} + {{- $isRoleBinding = true -}} +{{- end -}} +{{/* End of check */}} +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} +{{ if $isRoleBinding }} +## Example Usage + +```terraform +resource "{{.Name}}" "role_binding" { + resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + role = "owner" + subject = "john.doe@example.com" +} +``` +{{- else }} +{{ if .HasExamples -}} +## Example Usage + +{{- range .ExampleFiles }} + +{{ tffile . }} +{{- end }} +{{- end }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} + +{{- if $isRoleBinding }} + +## Import + +```terraform +# Only use the import statement, if you want to import an existing folder role assignment +import { + to = {{.Name}}.import-example + id = "${var.region},${var.resource_id},${var.role},${var.subject}" +} +``` + +{{- else }} +{{- if or .HasImport .HasImportIDConfig .HasImportIdentityConfig }} + +## Import + +Import is supported using the following syntax: +{{- end }} +{{- if .HasImportIdentityConfig }} + +In Terraform v1.12.0 and later, the [` + "`" + `import` + "`" + ` block](https://developer.hashicorp.com/terraform/language/import) can be used with the ` + "`" + `identity` + "`" + ` attribute, for example: + +{{tffile .ImportIdentityConfigFile }} + +{{ .IdentitySchemaMarkdown | trimspace }} +{{- end }} +{{- if .HasImportIDConfig }} + +In Terraform v1.5.0 and later, the [` + "`" + `import` + "`" + ` block](https://developer.hashicorp.com/terraform/language/import) can be used with the ` + "`" + `id` + "`" + ` attribute, for example: + +{{tffile .ImportIDConfigFile }} +{{- end }} +{{- if .HasImport }} + +The [` + "`" + `terraform import` + "`" + ` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: + +{{codefile "shell" .ImportFile }} +{{- end }} +{{- end }} From 16bff9e6344e65c2eba296564096603f78a8275c Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Tue, 21 Apr 2026 11:23:01 +0200 Subject: [PATCH 2/4] code review findings --- .../rolebindings-testing/acc_test_builder.go | 48 +++++++++++-------- .../internal/testdestroy/secretsmanager.go | 18 +++---- 2 files changed, 35 insertions(+), 31 deletions(-) diff --git a/stackit/internal/services/iam/rolebindings/v1/rolebindings-testing/acc_test_builder.go b/stackit/internal/services/iam/rolebindings/v1/rolebindings-testing/acc_test_builder.go index a6d9540fa..6936748a6 100644 --- a/stackit/internal/services/iam/rolebindings/v1/rolebindings-testing/acc_test_builder.go +++ b/stackit/internal/services/iam/rolebindings/v1/rolebindings-testing/acc_test_builder.go @@ -11,7 +11,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -func NewRoleBindingAccTestBuilder(tfProviderConfig, apiName, resourceType, resourceID string) *RoleBindingAccTestBuilder { +func NewRoleBindingAccTestBuilder(tfProviderConfig, apiName, resourceType, resourceID string) RoleBindingAccTestBuilderCreateStep { return &RoleBindingAccTestBuilder{ providerConfig: tfProviderConfig, resourceIdentifier: "stackit_" + apiName + "_" + resourceType + "_role_binding_v1." + resourceID, @@ -25,14 +25,27 @@ type RoleBindingAccTestBuilder struct { resourceIdentifier string // e.g. "stackit_secretsmanager_instance_role_binding.role_binding" // Note: Keep these steps here in the order they are executed later - createStep *resource.TestStep - importStep *resource.TestStep - updateStep *resource.TestStep + createStep resource.TestStep // required + importStep resource.TestStep // required + updateStep *resource.TestStep // optional +} + +type RoleBindingAccTestBuilderCreateStep interface { + CreateStep(tfConfig string, variables config.Variables, resourceIdResourceID, resourceIdField string) RoleBindingAccTestBuilderImportStep +} + +type RoleBindingAccTestBuilderImportStep interface { + ImportStep(variables config.Variables) RoleBindingAccTestBuilderFinalStep +} + +type RoleBindingAccTestBuilderFinalStep interface { + UpdateStep(tfConfig string, variables config.Variables, resourceIdResourceID, resourceIdField string) RoleBindingAccTestBuilderFinalStep // Optional + Build() resource.TestCase } // CreateStep is the first step in your acceptance test and creates the resources initially -func (b *RoleBindingAccTestBuilder) CreateStep(tfConfig string, variables config.Variables, resourceIdResourceID, resourceIdField string) *RoleBindingAccTestBuilder { - b.createStep = &resource.TestStep{ +func (b *RoleBindingAccTestBuilder) CreateStep(tfConfig string, variables config.Variables, resourceIdResourceID, resourceIdField string) RoleBindingAccTestBuilderImportStep { + b.createStep = resource.TestStep{ Config: b.providerConfig + "\n" + tfConfig, ConfigVariables: variables, Check: resource.ComposeAggregateTestCheckFunc( @@ -48,8 +61,8 @@ func (b *RoleBindingAccTestBuilder) CreateStep(tfConfig string, variables config } // ImportStep adds a terraform import test to your acceptance test case -func (b *RoleBindingAccTestBuilder) ImportStep(variables config.Variables) *RoleBindingAccTestBuilder { - b.importStep = &resource.TestStep{ +func (b *RoleBindingAccTestBuilder) ImportStep(variables config.Variables) RoleBindingAccTestBuilderFinalStep { + b.importStep = resource.TestStep{ ConfigVariables: variables, ResourceName: b.resourceIdentifier, ImportStateIdFunc: func(s *terraform.State) (string, error) { @@ -81,9 +94,9 @@ func (b *RoleBindingAccTestBuilder) ImportStep(variables config.Variables) *Role return b } -// UpdateStep is the first step in your acceptance test and updates the resources -func (b *RoleBindingAccTestBuilder) UpdateStep(tfConfig string, variables config.Variables, resourceIdResourceID, resourceIdField string) *RoleBindingAccTestBuilder { - b.createStep = &resource.TestStep{ +// UpdateStep adds a terraform update test to your acceptance test case +func (b *RoleBindingAccTestBuilder) UpdateStep(tfConfig string, variables config.Variables, resourceIdResourceID, resourceIdField string) RoleBindingAccTestBuilderFinalStep { + b.updateStep = &resource.TestStep{ Config: b.providerConfig + "\n" + tfConfig, ConfigVariables: variables, Check: resource.ComposeAggregateTestCheckFunc( @@ -102,15 +115,10 @@ func (b *RoleBindingAccTestBuilder) Build() resource.TestCase { tc := resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testdestroy.AccTestCheckDestroy, - Steps: []resource.TestStep{}, - } - - if b.createStep != nil { - tc.Steps = append(tc.Steps, *b.createStep) - } - - if b.importStep != nil { - tc.Steps = append(tc.Steps, *b.importStep) + Steps: []resource.TestStep{ + b.createStep, + b.importStep, + }, } if b.updateStep != nil { diff --git a/stackit/internal/testdestroy/secretsmanager.go b/stackit/internal/testdestroy/secretsmanager.go index aebfafc04..1c81830e7 100644 --- a/stackit/internal/testdestroy/secretsmanager.go +++ b/stackit/internal/testdestroy/secretsmanager.go @@ -3,11 +3,11 @@ package testdestroy import ( "context" "fmt" + "slices" "strings" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + secretsmanager "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1api" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" @@ -30,20 +30,16 @@ func testAccCheckSecretsManagerDestroy(s *terraform.State) error { instancesToDestroy = append(instancesToDestroy, instanceId) } - instancesResp, err := client.ListInstances(ctx, testutil.ProjectId).Execute() + instancesResp, err := client.DefaultAPI.ListInstances(ctx, testutil.ProjectId).Execute() if err != nil { return fmt.Errorf("getting instancesResp: %w", err) } - instances := *instancesResp.Instances - for i := range instances { - if instances[i].Id == nil { - continue - } - if utils.Contains(instancesToDestroy, *instances[i].Id) { - err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].Id) + for _, instance := range instancesResp.Instances { + if slices.Contains(instancesToDestroy, instance.Id) { + err := client.DefaultAPI.DeleteInstance(ctx, testutil.ProjectId, instance.Id).Execute() if err != nil { - return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].Id, err) + return fmt.Errorf("destroying instance %s during CheckDestroy: %w", instance.Id, err) } } } From 2f7bc12eb23645be05b79c4fd2b522ae70cf77b3 Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Tue, 21 Apr 2026 14:31:42 +0200 Subject: [PATCH 3/4] http 404 comments --- .../internal/services/iam/rolebindings/v1/generic/resource.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/stackit/internal/services/iam/rolebindings/v1/generic/resource.go b/stackit/internal/services/iam/rolebindings/v1/generic/resource.go index 693e92287..900b7e245 100644 --- a/stackit/internal/services/iam/rolebindings/v1/generic/resource.go +++ b/stackit/internal/services/iam/rolebindings/v1/generic/resource.go @@ -188,6 +188,7 @@ func (r *RoleBindingResource[C]) Read(ctx context.Context, req resource.ReadRequ ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "resource_id", resourceId) + // Note: The API won't return HTTP 404 errors here, at least there are no HTTP 404 errors documented for the distributed role binding API roleBindingResp, err := r.ExecReadRequest(ctx, r.apiClient, region, resourceId, model.Role.ValueString(), model.Subject.ValueString()) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error reading %s %s role binding", r.ApiName, r.ResourceType), fmt.Sprintf("Calling API: %v", err)) @@ -276,6 +277,7 @@ func (r *RoleBindingResource[C]) Delete(ctx context.Context, req resource.Delete ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "resource_id", resourceId) + // Note: The API won't return HTTP 404 errors here, at least there are no HTTP 404 errors documented for the distributed role binding API err := r.ExecDeleteRequest(ctx, r.apiClient, region, resourceId, model.Role.ValueString(), model.Subject.ValueString()) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error deleting %s %s role binding", r.ApiName, r.ResourceType), fmt.Sprintf("Calling API: %v", err)) From 1d5ef30b3492c949c3b60c042cdbac1d40cd3661 Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Thu, 23 Apr 2026 09:29:32 +0200 Subject: [PATCH 4/4] remove sleep --- .../internal/services/iam/rolebindings/v1/generic/resource.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/stackit/internal/services/iam/rolebindings/v1/generic/resource.go b/stackit/internal/services/iam/rolebindings/v1/generic/resource.go index 900b7e245..c854fcb1a 100644 --- a/stackit/internal/services/iam/rolebindings/v1/generic/resource.go +++ b/stackit/internal/services/iam/rolebindings/v1/generic/resource.go @@ -138,9 +138,6 @@ func (r *RoleBindingResource[C]) Create(ctx context.Context, req resource.Create ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "resource_id", resourceId) - // TODO: remove - time.Sleep(10 * time.Second) - roleBindingResp, err := r.ExecCreateRequest(ctx, r.apiClient, region, resourceId, model.Role.ValueString(), model.Subject.ValueString()) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error creating %s %s role binding", r.ApiName, r.ResourceType), fmt.Sprintf("Calling API: %v", err))