Skip to content

feat(iam, secretsmanager): add secretsmanager IAM rolebinding resources#1388

Open
rubenhoenle wants to merge 1 commit intomainfrom
feat/iam-rolebindings
Open

feat(iam, secretsmanager): add secretsmanager IAM rolebinding resources#1388
rubenhoenle wants to merge 1 commit intomainfrom
feat/iam-rolebindings

Conversation

@rubenhoenle
Copy link
Copy Markdown
Member

@rubenhoenle rubenhoenle commented Apr 17, 2026

Description

relates to STACKITTPR-497


IAM role binding API

This PR introduces the IAM role binding resources for the secretsmanager API. More services will adopt the role binding API in the future, so I've chosen a pretty generic approach for the implementation which allows us to implement further role binding resources with as little as possible effort.

Note: The IAM role binding API is a standardized API, so all upcoming role binding resources will look exactly the same. That's why using a generic approach is even possible at all 😅

Implementation of new role binding resource

Due to the generic implementation you only have to define some callback functions when implementing another role binding resource. It's pretty straightforward and is only ~40 lines of code.

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()
},
}
}

These two lines below determine the name of your resource, e.g. here it would be stackit_secretsmanager_instance_role_binding

ApiName: "secretsmanager",
ResourceType: "secret_group",

Acceptance tests for new role binding resource

For the acceptance tests I implemented a builder pattern which allows us to implement the tests for every resource without duplicating all the boilerplate code every time.

This is how to implement an acceptance test for the stackit_secretsmanager_instance_role_binding resource:

First you define your terraform config like you know it:

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" "role_binding" {
resource_id = stackit_secretsmanager_instance.instance.instance_id
role = var.role
subject = var.subject
}

Now you use the role binding acc test builder instead of writing all the boilerplate:

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)
}

Generation of docs

But not only implementation of new role binding resources is trivial and less effort. You also don't have to provide examples and import statement for new resources on your own. Instead they are generated for you automatically:

## Example Usage
```terraform
resource "{{.Name}}" "role_binding" {
resource_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
role = "owner"
subject = "john.doe@example.com"
}

## 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}"
}


Checklist

  • Issue was linked above
  • Code format was applied: make fmt
  • Examples were added / adjusted (see examples/ directory)
  • Docs are up-to-date: make generate-docs (will be checked by CI)
  • Unit tests got implemented or updated
  • Acceptance tests got implemented or updated (see e.g. here)
  • Unit tests are passing: make test (will be checked by CI)
  • No linter issues: make lint (will be checked by CI)

@rubenhoenle rubenhoenle force-pushed the feat/iam-rolebindings branch from 95496f9 to e75b053 Compare April 17, 2026 13:15
@rubenhoenle rubenhoenle marked this pull request as ready for review April 17, 2026 16:07
@rubenhoenle rubenhoenle requested a review from a team as a code owner April 17, 2026 16:07
ctx = tflog.SetField(ctx, "region", region)
ctx = tflog.SetField(ctx, "resource_id", resourceId)

// TODO: remove
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolve before merge

Comment thread .github/docs/contribution-guide/resource.go Outdated
@rubenhoenle rubenhoenle force-pushed the feat/iam-rolebindings branch from e48153e to 5e12fb9 Compare April 20, 2026 09:43
@github-actions
Copy link
Copy Markdown

Merging this branch will increase overall coverage

Impacted Packages Coverage Δ 🤖
github.com/stackitcloud/terraform-provider-stackit/stackit 1.27% (-0.01%) 👎
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1 0.00% (ø)
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/generic 8.94% (+8.94%) 👍
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/rolebindings-testing 0.00% (ø)
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager 0.00% (ø)
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager 0.00% (ø)
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils 77.78% (ø)
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testdestroy 0.00% (ø)

Coverage by file

Changed files (no unit tests)

Changed File Coverage Δ Total Covered Missed 🤖
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/generic/resource.go 8.94% (+8.94%) 123 (+123) 11 (+11) 112 (+112) 👍
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/rolebindings-testing/acc_test_builder.go 0.00% (ø) 28 (+28) 0 28 (+28)
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/rolebindings.go 0.00% (ø) 1 (+1) 0 1 (+1)
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/instance.go 0.00% (ø) 9 (+9) 0 9 (+9)
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/secret_group.go 0.00% (ø) 9 (+9) 0 9 (+9)
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils/util.go 77.78% (ø) 18 (+9) 14 (+7) 4 (+2)
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testdestroy/acc_destroy.go 0.00% (ø) 6 (+6) 0 6 (+6)
github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testdestroy/secretsmanager.go 0.00% (ø) 22 (+22) 0 22 (+22)
github.com/stackitcloud/terraform-provider-stackit/stackit/provider.go 1.27% (-0.01%) 158 (+1) 2 156 (+1) 👎

Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code.

Changed unit test files

  • github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/generic/resource_test.go
  • github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iam/rolebindings/v1/services/secretsmanager/iam_rolebindings_secretsmanager_acc_test.go
  • github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go
  • github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils/util_test.go

Comment on lines +191 to +195
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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in #1303 a contributor implemented, that on read a 404 leads to the resources being removed from the state.
I'd add this here. Not every ExecReadRequest will return a 404, but in case it does not it doesn't matter.

ctx = tflog.SetField(ctx, "resource_id", resourceId)

err := r.ExecDeleteRequest(ctx, r.apiClient, region, resourceId, model.Role.ValueString(), model.Subject.ValueString())
if err != nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also add an 404 check here and just return early on 404, TF will then remove the resource from state

return b
}

// UpdateStep is the first step in your acceptance test and updates the resources
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like copy paste error from CreateStep

Comment on lines +108 to +116
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 {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd fail on these nil checks instead. The generic resource looks like these steps are required and thus should also be required in the test.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If these test steps are required: we could change the builder API to make it more clear that it's not optional to add these steps.

Comment thread stackit/internal/services/iam/rolebindings/v1/generic/resource.go
if instances[i].Id == nil {
continue
}
if utils.Contains(instancesToDestroy, *instances[i].Id) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slices.Contains

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code was only moved by me, but i will adjust it

Comment thread stackit/internal/testdestroy/acc_destroy.go
Comment on lines +8 to +16
{{/* 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 -}}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe it would be worth it to create an issue at tfplugin-docs to allow adding custom template funcs

@stackitcloud stackitcloud deleted a comment from rubenhoenle Apr 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants