From 1880d48ffd24b3c4ce3ad4c948a70227cb2b73d9 Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Thu, 23 Apr 2026 09:27:11 +0200 Subject: [PATCH 1/2] feat(iam, secretsmanager): add secretsmanager IAM rolebinding datasources relates to STACKITTPR-641 --- ...ecretsmanager_instance_role_bindings_v1.md | 46 +++++ ...tsmanager_secret_group_role_bindings_v1.md | 46 +++++ ...secretsmanager_instance_role_binding_v1.md | 18 ++ ...etsmanager_secret_group_role_binding_v1.md | 18 ++ .../iam/rolebindings/v1/generic/datasource.go | 170 ++++++++++++++++++ .../v1/generic/datasource_test.go | 110 ++++++++++++ .../rolebindings/v1/generic/resource_test.go | 10 ++ .../rolebindings-testing/acc_test_builder.go | 49 ++++- .../iam/rolebindings/v1/rolebindings.go | 15 +- ...am_rolebindings_secretsmanager_acc_test.go | 1 + .../v1/services/secretsmanager/instance.go | 25 +++ .../services/secretsmanager/secret_group.go | 25 +++ stackit/internal/utils/utils.go | 8 + stackit/internal/utils/utils_test.go | 41 +++++ stackit/provider.go | 1 + templates/data-sources.md.tmpl | 44 +++++ templates/resources.md.tmpl | 6 +- 17 files changed, 621 insertions(+), 12 deletions(-) create mode 100644 docs/data-sources/secretsmanager_instance_role_bindings_v1.md create mode 100644 docs/data-sources/secretsmanager_secret_group_role_bindings_v1.md create mode 100644 stackit/internal/services/iam/rolebindings/v1/generic/datasource.go create mode 100644 stackit/internal/services/iam/rolebindings/v1/generic/datasource_test.go create mode 100644 templates/data-sources.md.tmpl diff --git a/docs/data-sources/secretsmanager_instance_role_bindings_v1.md b/docs/data-sources/secretsmanager_instance_role_bindings_v1.md new file mode 100644 index 000000000..77d250b25 --- /dev/null +++ b/docs/data-sources/secretsmanager_instance_role_bindings_v1.md @@ -0,0 +1,46 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_secretsmanager_instance_role_bindings_v1 Data Source - stackit" +subcategory: "" +description: |- + IAM role binding datasource schema. + ~> This datasource 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_bindings_v1 (Data Source) + +IAM role binding datasource schema. + +~> This datasource 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. + +## Example Usage + +```terraform +data "stackit_secretsmanager_instance_role_bindings_v1" "role_binding" { + resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `resource_id` (String) The identifier of the resource to get the role bindings for. + +### 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_bindings` (Attributes List) List of role bindings. (see [below for nested schema](#nestedatt--role_bindings)) + + +### Nested Schema for `role_bindings` + +Read-Only: + +- `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. diff --git a/docs/data-sources/secretsmanager_secret_group_role_bindings_v1.md b/docs/data-sources/secretsmanager_secret_group_role_bindings_v1.md new file mode 100644 index 000000000..0125bd201 --- /dev/null +++ b/docs/data-sources/secretsmanager_secret_group_role_bindings_v1.md @@ -0,0 +1,46 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_secretsmanager_secret_group_role_bindings_v1 Data Source - stackit" +subcategory: "" +description: |- + IAM role binding datasource schema. + ~> This datasource 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_bindings_v1 (Data Source) + +IAM role binding datasource schema. + +~> This datasource 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. + +## Example Usage + +```terraform +data "stackit_secretsmanager_secret_group_role_bindings_v1" "role_binding" { + resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `resource_id` (String) The identifier of the resource to get the role bindings for. + +### 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_bindings` (Attributes List) List of role bindings. (see [below for nested schema](#nestedatt--role_bindings)) + + +### Nested Schema for `role_bindings` + +Read-Only: + +- `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. diff --git a/docs/resources/secretsmanager_instance_role_binding_v1.md b/docs/resources/secretsmanager_instance_role_binding_v1.md index a93536fbe..caa891d30 100644 --- a/docs/resources/secretsmanager_instance_role_binding_v1.md +++ b/docs/resources/secretsmanager_instance_role_binding_v1.md @@ -13,7 +13,15 @@ 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. +## Example Usage +```terraform +resource "stackit_secretsmanager_instance_role_binding_v1" "role_binding" { + resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + role = "owner" + subject = "john.doe@example.com" +} +``` ## Schema @@ -31,3 +39,13 @@ IAM role binding resource schema. ### Read-Only - `id` (String) Terraform's internal resource identifier. It is structured as "`region`,`resource_id`,`role`,`subject`". + +## Import + +```terraform +# Only use the import statement if you want to import an existing role binding +import { + to = stackit_secretsmanager_instance_role_binding_v1.import_example + id = "${var.region},${var.resource_id},${var.role},${var.subject}" +} +``` diff --git a/docs/resources/secretsmanager_secret_group_role_binding_v1.md b/docs/resources/secretsmanager_secret_group_role_binding_v1.md index 32cfa5603..38e1a10ba 100644 --- a/docs/resources/secretsmanager_secret_group_role_binding_v1.md +++ b/docs/resources/secretsmanager_secret_group_role_binding_v1.md @@ -13,7 +13,15 @@ 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. +## Example Usage +```terraform +resource "stackit_secretsmanager_secret_group_role_binding_v1" "role_binding" { + resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + role = "owner" + subject = "john.doe@example.com" +} +``` ## Schema @@ -31,3 +39,13 @@ IAM role binding resource schema. ### Read-Only - `id` (String) Terraform's internal resource identifier. It is structured as "`region`,`resource_id`,`role`,`subject`". + +## Import + +```terraform +# Only use the import statement if you want to import an existing role binding +import { + to = stackit_secretsmanager_secret_group_role_binding_v1.import_example + id = "${var.region},${var.resource_id},${var.role},${var.subject}" +} +``` diff --git a/stackit/internal/services/iam/rolebindings/v1/generic/datasource.go b/stackit/internal/services/iam/rolebindings/v1/generic/datasource.go new file mode 100644 index 000000000..9779eeca8 --- /dev/null +++ b/stackit/internal/services/iam/rolebindings/v1/generic/datasource.go @@ -0,0 +1,170 @@ +package generic + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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 ( + _ datasource.DataSource = &RoleBindingDatasource[secretsmanagerV1Alpha.APIClient]{} +) + +type DatasourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + Region types.String `tfsdk:"region"` + ResourceId types.String `tfsdk:"resource_id"` + RoleBindings []nestedRoleBinding `tfsdk:"role_bindings"` +} + +type nestedRoleBinding struct { + Role types.String `tfsdk:"role"` + Subject types.String `tfsdk:"subject"` +} + +// RoleBindingDatasource is the resource implementation. +type RoleBindingDatasource[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 string) ([]GenericRoleBindingResponse, error) +} + +// Metadata returns the resource type name. +func (r *RoleBindingDatasource[C]) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_%s_%s_role_bindings_v1", req.ProviderTypeName, r.ApiName, r.ResourceType) +} + +// Configure adds the provider configured client to the resource. +func (r *RoleBindingDatasource[C]) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.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", 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 *RoleBindingDatasource[C]) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: features.AddExperimentDescription("IAM role binding datasource schema.", features.IamExperiment, core.Datasource), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal resource identifier. It is structured as \"`region`,`resource_id`\".", + Computed: true, + }, + "resource_id": schema.StringAttribute{ + Description: "The identifier of the resource to get the role bindings for.", + Required: true, + }, + "region": schema.StringAttribute{ + Optional: true, + // the region cannot be found automatically, so it has to be passed + Description: "The resource region. If not defined, the provider region is used.", + }, + "role_bindings": schema.ListNestedAttribute{ + Computed: true, + Description: "List of role bindings.", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "role": schema.StringAttribute{ + Computed: true, + Description: "A valid role defined for the resource.", + }, + "subject": schema.StringAttribute{ + Computed: true, + Description: "Identifier of user, service account or client. Usually email address or name in case of clients.", + }, + }, + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (r *RoleBindingDatasource[C]) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model DatasourceModel + diags := req.Config.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) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error reading %s %s role bindings", r.ApiName, r.ResourceType), fmt.Sprintf("Calling API: %v", err)) + return + } + + ctx = core.LogResponse(ctx) + + // Map response body to schema + err = mapDatasourceFields(roleBindingResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error reading %s %s role bindings", 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 bindings read", r.ApiName, r.ResourceType)) +} + +func mapDatasourceFields(resp []GenericRoleBindingResponse, model *DatasourceModel, region string) error { + if model == nil { + return fmt.Errorf("nil model") + } + + model.Id = utils.BuildInternalTerraformId(region, model.ResourceId.ValueString()) + model.Region = types.StringValue(region) + + model.RoleBindings = make([]nestedRoleBinding, len(resp)) + for i, roleBinding := range resp { + model.RoleBindings[i] = nestedRoleBinding{ + Role: types.StringValue(roleBinding.GetRole()), + Subject: types.StringValue(roleBinding.GetSubject()), + } + } + + return nil +} diff --git a/stackit/internal/services/iam/rolebindings/v1/generic/datasource_test.go b/stackit/internal/services/iam/rolebindings/v1/generic/datasource_test.go new file mode 100644 index 000000000..24d5e70d2 --- /dev/null +++ b/stackit/internal/services/iam/rolebindings/v1/generic/datasource_test.go @@ -0,0 +1,110 @@ +package generic + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/types" + secretsmanagerV1Alpha "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1alphaapi" +) + +func Test_mapDatasourceFields(t *testing.T) { + const testRegion = "eu01" + testResourceId := uuid.New().String() + + type args struct { + resp []GenericRoleBindingResponse + model *DatasourceModel + region string + } + tests := []struct { + name string + args args + wantModel *DatasourceModel + wantErr bool + }{ + { + name: "default", + args: args{ + resp: func() []GenericRoleBindingResponse { + roleBinding1 := secretsmanagerV1Alpha.RoleBinding{ + Role: "owner", + Subject: "john.doe@example.com", + } + roleBinding2 := secretsmanagerV1Alpha.RoleBinding{ + Role: "editor", + Subject: "jane.doe@example.com", + } + + return []GenericRoleBindingResponse{ + &roleBinding1, + &roleBinding2, + } + }(), + model: &DatasourceModel{ + ResourceId: types.StringValue(testResourceId), + }, + region: testRegion, + }, + wantErr: false, + wantModel: &DatasourceModel{ + Id: types.StringValue(fmt.Sprintf("%s,%s", testRegion, testResourceId)), + ResourceId: types.StringValue(testResourceId), + Region: types.StringValue(testRegion), + RoleBindings: []nestedRoleBinding{ + { + Role: types.StringValue("owner"), + Subject: types.StringValue("john.doe@example.com"), + }, + { + Role: types.StringValue("editor"), + Subject: types.StringValue("jane.doe@example.com"), + }, + }, + }, + }, + { + name: "response is nil", + args: args{ + resp: nil, + model: &DatasourceModel{ + ResourceId: types.StringValue(testResourceId), + }, + region: testRegion, + }, + wantErr: false, + wantModel: &DatasourceModel{ + Id: types.StringValue(fmt.Sprintf("%s,%s", testRegion, testResourceId)), + ResourceId: types.StringValue(testResourceId), + Region: types.StringValue(testRegion), + RoleBindings: []nestedRoleBinding{}, + }, + }, + { + name: "model is nil", + args: args{ + model: nil, + resp: []GenericRoleBindingResponse{}, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := mapDatasourceFields(tt.args.resp, tt.args.model, tt.args.region); (err != nil) != tt.wantErr { + t.Errorf("mapDatasourceFields() error = %v, wantErr %v", err, tt.wantErr) + } + + if tt.wantErr { + return + } + + diff := cmp.Diff(tt.args.model, tt.wantModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/stackit/internal/services/iam/rolebindings/v1/generic/resource_test.go b/stackit/internal/services/iam/rolebindings/v1/generic/resource_test.go index 9936a0b5f..a87bafa8f 100644 --- a/stackit/internal/services/iam/rolebindings/v1/generic/resource_test.go +++ b/stackit/internal/services/iam/rolebindings/v1/generic/resource_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/types" secretsmanagerV1Alpha "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager/v1alphaapi" @@ -67,6 +68,15 @@ func Test_mapFields(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) } + + if tt.wantErr { + return + } + + diff := cmp.Diff(tt.args.model, tt.wantModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } }) } } 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 6936748a6..6eccb177b 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 @@ -15,6 +15,8 @@ func NewRoleBindingAccTestBuilder(tfProviderConfig, apiName, resourceType, resou return &RoleBindingAccTestBuilder{ providerConfig: tfProviderConfig, resourceIdentifier: "stackit_" + apiName + "_" + resourceType + "_role_binding_v1." + resourceID, + apiName: apiName, + resourceType: resourceType, } } @@ -23,15 +25,22 @@ type RoleBindingAccTestBuilder struct { providerConfig string resourceIdentifier string // e.g. "stackit_secretsmanager_instance_role_binding.role_binding" + apiName string // e.g. "secretsmanager" + resourceType string // e.g. "instance" // Note: Keep these steps here in the order they are executed later - createStep resource.TestStep // required - importStep resource.TestStep // required - updateStep *resource.TestStep // optional + createStep resource.TestStep // required + datasourceStep resource.TestStep // required + importStep resource.TestStep // required + updateStep *resource.TestStep // optional } type RoleBindingAccTestBuilderCreateStep interface { - CreateStep(tfConfig string, variables config.Variables, resourceIdResourceID, resourceIdField string) RoleBindingAccTestBuilderImportStep + CreateStep(tfConfig string, variables config.Variables, resourceIdResourceID, resourceIdField string) RoleBindingAccTestBuilderDatasourceStep +} + +type RoleBindingAccTestBuilderDatasourceStep interface { + DatasourceStep(tfConfig string, variables config.Variables) RoleBindingAccTestBuilderImportStep } type RoleBindingAccTestBuilderImportStep interface { @@ -44,7 +53,7 @@ type RoleBindingAccTestBuilderFinalStep interface { } // 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) RoleBindingAccTestBuilderImportStep { +func (b *RoleBindingAccTestBuilder) CreateStep(tfConfig string, variables config.Variables, resourceIdResourceID, resourceIdField string) RoleBindingAccTestBuilderDatasourceStep { b.createStep = resource.TestStep{ Config: b.providerConfig + "\n" + tfConfig, ConfigVariables: variables, @@ -55,12 +64,38 @@ func (b *RoleBindingAccTestBuilder) CreateStep(tfConfig string, variables config ), resource.TestCheckResourceAttr(b.resourceIdentifier, "role", testutil.ConvertConfigVariable(variables["role"])), resource.TestCheckResourceAttr(b.resourceIdentifier, "subject", testutil.ConvertConfigVariable(variables["subject"])), + resource.TestCheckResourceAttr(b.resourceIdentifier, "region", testutil.Region), + ), + } + return b +} + +// DatasourceStep is the second step in your acceptance test and verifies the output of the datasource +func (b *RoleBindingAccTestBuilder) DatasourceStep(tfConfig string, variables config.Variables) RoleBindingAccTestBuilderImportStep { + datasourceIdentifier := "data.stackit_" + b.apiName + "_" + b.resourceType + "_role_bindings_v1.role_bindings" + + b.datasourceStep = resource.TestStep{ + Config: fmt.Sprintf(`%s + + %s + + data "stackit_%s_%s_role_bindings_v1" "role_bindings" { + resource_id = %s.resource_id + } + `, b.providerConfig, tfConfig, b.apiName, b.resourceType, b.resourceIdentifier), + ConfigVariables: variables, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(b.resourceIdentifier, "resource_id", datasourceIdentifier, "resource_id"), + resource.TestCheckResourceAttr(datasourceIdentifier, "role_bindings.#", "1"), + resource.TestCheckResourceAttr(datasourceIdentifier, "role_bindings.0.role", testutil.ConvertConfigVariable(variables["role"])), + resource.TestCheckResourceAttr(datasourceIdentifier, "role_bindings.0.subject", testutil.ConvertConfigVariable(variables["subject"])), + resource.TestCheckResourceAttr(datasourceIdentifier, "region", testutil.Region), ), } return b } -// ImportStep adds a terraform import test to your acceptance test case +// ImportStep is the third step in your acceptance test and verifies the terraform import is working properly func (b *RoleBindingAccTestBuilder) ImportStep(variables config.Variables) RoleBindingAccTestBuilderFinalStep { b.importStep = resource.TestStep{ ConfigVariables: variables, @@ -106,6 +141,7 @@ func (b *RoleBindingAccTestBuilder) UpdateStep(tfConfig string, variables config ), resource.TestCheckResourceAttr(b.resourceIdentifier, "role", testutil.ConvertConfigVariable(variables["role"])), resource.TestCheckResourceAttr(b.resourceIdentifier, "subject", testutil.ConvertConfigVariable(variables["subject"])), + resource.TestCheckResourceAttr(b.resourceIdentifier, "region", testutil.Region), ), } return b @@ -117,6 +153,7 @@ func (b *RoleBindingAccTestBuilder) Build() resource.TestCase { CheckDestroy: testdestroy.AccTestCheckDestroy, Steps: []resource.TestStep{ b.createStep, + b.datasourceStep, b.importStep, }, } diff --git a/stackit/internal/services/iam/rolebindings/v1/rolebindings.go b/stackit/internal/services/iam/rolebindings/v1/rolebindings.go index 2e1cf7a56..22efdca92 100644 --- a/stackit/internal/services/iam/rolebindings/v1/rolebindings.go +++ b/stackit/internal/services/iam/rolebindings/v1/rolebindings.go @@ -1,16 +1,25 @@ package v1 import ( + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/resource" - secretsmanager2 "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager" + "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, + secretsmanager.NewSecretsmanagerInstanceRoleBindingResource, + secretsmanager.NewSecretsmanagerSecretGroupRoleBindingResource, + } +} + +func NewRoleBindingsDatasources() []func() datasource.DataSource { + return []func() datasource.DataSource{ + // secretsmanager + secretsmanager.NewSecretsmanagerInstanceRoleBindingsDatasource, + secretsmanager.NewSecretsmanagerSecretGroupRoleBindingsDatasource, } } 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 index 3fd62873a..0849f7f6d 100644 --- 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 @@ -38,6 +38,7 @@ func TestAccSecretsManagerInstanceRoleBindings(t *testing.T) { tc := rolebindings_testing.NewRoleBindingAccTestBuilder(providerConfig, "secretsmanager", "instance", "role_binding"). CreateStep(instanceConfig, variables, "stackit_secretsmanager_instance.instance", "instance_id"). + DatasourceStep(instanceConfig, variables). ImportStep(variables). UpdateStep(instanceConfig, variablesUpdated(), "stackit_secretsmanager_instance.instance", "instance_id"). Build() diff --git a/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/instance.go b/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/instance.go index f9da68eed..e32e83026 100644 --- a/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/instance.go +++ b/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/instance.go @@ -3,9 +3,12 @@ package secretsmanager import ( "context" + "github.com/hashicorp/terraform-plugin-framework/datasource" "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/utils" + "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" @@ -50,3 +53,25 @@ func NewSecretsmanagerInstanceRoleBindingResource() resource.Resource { }, } } + +func NewSecretsmanagerInstanceRoleBindingsDatasource() datasource.DataSource { + return &generic.RoleBindingDatasource[secretsmanagerV1Alpha.APIClient]{ + ApiName: "secretsmanager", + ResourceType: "instance", + ApiClientFactory: secretsmanagerUtils.ConfigureV1AlphaClient, + ExecReadRequest: func(ctx context.Context, client *secretsmanagerV1Alpha.APIClient, region, resourceId string) ([]generic.GenericRoleBindingResponse, error) { + resp, err := client.DefaultAPI.ListInstanceRoleBindings(ctx, region, resourceId).Execute() + if err != nil { + return nil, err + } + + if resp == nil { + return nil, nil + } + + return utils.Map(resp.RoleBindings, func(t secretsmanagerV1Alpha.RoleBinding) generic.GenericRoleBindingResponse { + return &t + }), nil + }, + } +} 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 index 2427ae340..329811926 100644 --- a/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/secret_group.go +++ b/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/secret_group.go @@ -3,9 +3,12 @@ package secretsmanager import ( "context" + "github.com/hashicorp/terraform-plugin-framework/datasource" "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/utils" + "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" @@ -50,3 +53,25 @@ func NewSecretsmanagerSecretGroupRoleBindingResource() resource.Resource { }, } } + +func NewSecretsmanagerSecretGroupRoleBindingsDatasource() datasource.DataSource { + return &generic.RoleBindingDatasource[secretsmanagerV1Alpha.APIClient]{ + ApiName: "secretsmanager", + ResourceType: "secret_group", + ApiClientFactory: secretsmanagerUtils.ConfigureV1AlphaClient, + ExecReadRequest: func(ctx context.Context, client *secretsmanagerV1Alpha.APIClient, region, resourceId string) ([]generic.GenericRoleBindingResponse, error) { + resp, err := client.DefaultAPI.ListSecretGroupRoleBindings(ctx, region, resourceId).Execute() + if err != nil { + return nil, err + } + + if resp == nil { + return nil, nil + } + + return utils.Map(resp.RoleBindings, func(t secretsmanagerV1Alpha.RoleBinding) generic.GenericRoleBindingResponse { + return &t + }), nil + }, + } +} diff --git a/stackit/internal/utils/utils.go b/stackit/internal/utils/utils.go index 4e019ad59..304dd0bd7 100644 --- a/stackit/internal/utils/utils.go +++ b/stackit/internal/utils/utils.go @@ -214,3 +214,11 @@ func PrettyApiErr(ctx context.Context, diags *diag.Diagnostics, err error) strin } return err.Error() } + +func Map[T, U any](input []T, mapFn func(T) U) []U { + values := make([]U, len(input)) + for i := range input { + values[i] = mapFn(input[i]) + } + return values +} diff --git a/stackit/internal/utils/utils_test.go b/stackit/internal/utils/utils_test.go index 365586736..30e61378d 100644 --- a/stackit/internal/utils/utils_test.go +++ b/stackit/internal/utils/utils_test.go @@ -628,3 +628,44 @@ func TestPrettyApiErr(t *testing.T) { }) } } + +func TestMap(t *testing.T) { + type args[T any, U any] struct { + input []T + mapFn func(T) U + } + type testCase[T any, U any] struct { + name string + args args[T, U] + want []U + } + tests := []testCase[string, *string]{ + { + name: "default", + args: args[string, *string]{ + input: []string{"foo", "bar"}, + mapFn: func(s string) *string { + return new(s) + }, + }, + want: []*string{new("foo"), new("bar")}, + }, + { + name: "input slice is nil", + args: args[string, *string]{ + input: nil, + mapFn: func(s string) *string { + return new(s) + }, + }, + want: []*string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Map(tt.args.input, tt.args.mapFn); !reflect.DeepEqual(got, tt.want) { + t.Errorf("Map() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/stackit/provider.go b/stackit/provider.go index 3abeb1167..9b466d5c5 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -701,6 +701,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource serverUpdateEnable.NewServerUpdateEnableDataSource, } dataSources = append(dataSources, customRole.NewCustomRoleDataSources()...) + dataSources = append(dataSources, iamRoleBindingsV1.NewRoleBindingsDatasources()...) return dataSources } diff --git a/templates/data-sources.md.tmpl b/templates/data-sources.md.tmpl new file mode 100644 index 000000000..f1c505a26 --- /dev/null +++ b/templates/data-sources.md.tmpl @@ -0,0 +1,44 @@ +--- +# 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_bindings_v1" -}} +{{- $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 +data "{{.Name}}" "role_binding" { + resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` +{{- else }} +{{ if .HasExamples -}} +## Example Usage + +{{- range .ExampleFiles }} + +{{ tffile . }} +{{- end }} +{{- end }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources.md.tmpl b/templates/resources.md.tmpl index 91fc9707b..65e2b22c6 100644 --- a/templates/resources.md.tmpl +++ b/templates/resources.md.tmpl @@ -8,7 +8,7 @@ description: |- {{/* 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" -}} +{{- $parts := split .Name "_role_binding_v1" -}} {{- $lastItem := "" -}} {{- range $parts -}} @@ -50,9 +50,9 @@ resource "{{.Name}}" "role_binding" { ## Import ```terraform -# Only use the import statement, if you want to import an existing folder role assignment +# Only use the import statement if you want to import an existing role binding import { - to = {{.Name}}.import-example + to = {{.Name}}.import_example id = "${var.region},${var.resource_id},${var.role},${var.subject}" } ``` From b9bbc20e5185484849257a7c44da8352fab6354d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20H=C3=B6nle?= Date: Tue, 28 Apr 2026 11:14:26 +0200 Subject: [PATCH 2/2] Update stackit/internal/utils/utils.go Co-authored-by: cgoetz-inovex --- stackit/internal/utils/utils.go | 1 + 1 file changed, 1 insertion(+) diff --git a/stackit/internal/utils/utils.go b/stackit/internal/utils/utils.go index 304dd0bd7..da710a75d 100644 --- a/stackit/internal/utils/utils.go +++ b/stackit/internal/utils/utils.go @@ -215,6 +215,7 @@ func PrettyApiErr(ctx context.Context, diags *diag.Diagnostics, err error) strin return err.Error() } +// Map applies mapFn to each element in input and collects the results in a new slice func Map[T, U any](input []T, mapFn func(T) U) []U { values := make([]U, len(input)) for i := range input {