From 7f077349d03ab5a69406d825aba3cd968eff4a95 Mon Sep 17 00:00:00 2001 From: Jonas Schlecht Date: Thu, 27 Aug 2026 14:51:35 +0200 Subject: [PATCH] feat(rabbitmq): add roles support to rabbitmq credential resource Relates to STACKITTPR-815 --- docs/resources/rabbitmq_credential.md | 7 +++ .../stackit_rabbitmq_credential/resource.tf | 6 +++ .../services/rabbitmq/credential/resource.go | 48 ++++++++++++++++- .../rabbitmq/credential/resource_test.go | 54 +++++++++++++++++++ .../services/rabbitmq/rabbitmq_acc_test.go | 8 ++- 5 files changed, 119 insertions(+), 4 deletions(-) diff --git a/docs/resources/rabbitmq_credential.md b/docs/resources/rabbitmq_credential.md index af031b930..319d39d8a 100644 --- a/docs/resources/rabbitmq_credential.md +++ b/docs/resources/rabbitmq_credential.md @@ -18,6 +18,12 @@ resource "stackit_rabbitmq_credential" "example" { instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } +resource "stackit_rabbitmq_credential" "example_monitoring" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + roles = ["monitoring"] +} + resource "time_rotating" "rotate" { rotation_days = 80 } @@ -43,6 +49,7 @@ resource "stackit_rabbitmq_credential" "example" { ### Optional - `region` (String) The resource region. If not defined, the provider region is used. +- `roles` (Set of String) A list of roles to assign to the generated credentials. If not provided, the standard default role 'policymaker' will be assigned. Possible values are: `management`, `policymaker`, `monitoring`, `administrator`.. - `rotate_when_changed` (Map of String) A map of arbitrary key/value pairs that will force recreation of the resource when they change, enabling resource rotation based on external conditions such as a rotating timestamp. Changing this forces a new resource to be created. ### Read-Only diff --git a/examples/resources/stackit_rabbitmq_credential/resource.tf b/examples/resources/stackit_rabbitmq_credential/resource.tf index ef238431f..f4ba08f50 100644 --- a/examples/resources/stackit_rabbitmq_credential/resource.tf +++ b/examples/resources/stackit_rabbitmq_credential/resource.tf @@ -3,6 +3,12 @@ resource "stackit_rabbitmq_credential" "example" { instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" } +resource "stackit_rabbitmq_credential" "example_monitoring" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + instance_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + roles = ["monitoring"] +} + resource "time_rotating" "rotate" { rotation_days = 80 } diff --git a/stackit/internal/services/rabbitmq/credential/resource.go b/stackit/internal/services/rabbitmq/credential/resource.go index aaaa38860..d6672a59d 100644 --- a/stackit/internal/services/rabbitmq/credential/resource.go +++ b/stackit/internal/services/rabbitmq/credential/resource.go @@ -7,7 +7,10 @@ import ( "net/http" "strings" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" @@ -25,6 +28,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" rabbitmq "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api" "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/v2api/wait" ) @@ -35,6 +39,8 @@ var ( _ resource.ResourceWithConfigure = &credentialResource{} _ resource.ResourceWithImportState = &credentialResource{} _ resource.ResourceWithModifyPlan = &credentialResource{} + + roleOptions = sdkUtils.EnumSliceToStringSlice(rabbitmq.AllowedCredentialsParametersRolesInnerEnumValues) ) type Model struct { @@ -53,6 +59,7 @@ type Model struct { Uri types.String `tfsdk:"uri"` Uris types.List `tfsdk:"uris"` Username types.String `tfsdk:"username"` + Roles types.Set `tfsdk:"roles"` // RotateWhenChanged is a map of arbitrary key/value pairs that will force // recreation of the resource when they change, enabling resource rotation based on // external conditions such as a rotating timestamp. Changing this forces a new @@ -225,6 +232,21 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, mapplanmodifier.RequiresReplace(), }, }, + "roles": schema.SetAttribute{ + Description: "A list of roles to assign to the generated credentials. " + + "If not provided, the standard default role 'policymaker' will be assigned. " + + fmt.Sprintf("%s.", utils.FormatPossibleValues(roleOptions...)), + ElementType: types.StringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.ValueStringsAre( + stringvalidator.OneOf(roleOptions...), + ), + }, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, + }, "region": schema.StringAttribute{ Optional: true, // must be computed to allow for storing the override value from the provider @@ -256,8 +278,30 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ ctx = tflog.SetField(ctx, "instance_id", instanceId) ctx = tflog.SetField(ctx, "region", region) - // Create new recordset - credentialsResp, err := r.client.DefaultAPI.CreateCredentials(ctx, projectId, region, instanceId).Execute() + // Create new credential + createReq := r.client.DefaultAPI.CreateCredentials(ctx, projectId, region, instanceId) + + if !utils.IsUndefined(model.Roles) { + var roles []string + diags = model.Roles.ElementsAs(ctx, &roles, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + sdkRoles := make([]rabbitmq.CredentialsParametersRolesInner, len(roles)) + for i, r := range roles { + sdkRoles[i] = rabbitmq.CredentialsParametersRolesInner(r) + } + + createReq = createReq.CreateCredentialsPayload(rabbitmq.CreateCredentialsPayload{ + Parameters: &rabbitmq.CredentialsParameters{ + Roles: sdkRoles, + }, + }) + } + + credentialsResp, err := createReq.Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) return diff --git a/stackit/internal/services/rabbitmq/credential/resource_test.go b/stackit/internal/services/rabbitmq/credential/resource_test.go index 6e955ecce..346d7ff4c 100644 --- a/stackit/internal/services/rabbitmq/credential/resource_test.go +++ b/stackit/internal/services/rabbitmq/credential/resource_test.go @@ -26,6 +26,7 @@ func TestMapFields(t *testing.T) { InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), RotateWhenChanged: types.MapNull(types.StringType), + Roles: types.SetNull(types.StringType), }, &rabbitmq.CredentialsResponse{ Id: "cid", @@ -47,6 +48,7 @@ func TestMapFields(t *testing.T) { Uri: types.StringNull(), Uris: types.ListNull(types.StringType), Username: types.StringValue(""), + Roles: types.SetNull(types.StringType), RotateWhenChanged: types.MapNull(types.StringType), }, true, @@ -57,6 +59,7 @@ func TestMapFields(t *testing.T) { InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), RotateWhenChanged: types.MapNull(types.StringType), + Roles: types.SetNull(types.StringType), }, &rabbitmq.CredentialsResponse{ Id: "cid", @@ -109,6 +112,7 @@ func TestMapFields(t *testing.T) { types.StringValue(""), }), Username: types.StringValue("username"), + Roles: types.SetNull(types.StringType), RotateWhenChanged: types.MapNull(types.StringType), }, true, @@ -134,6 +138,7 @@ func TestMapFields(t *testing.T) { types.StringValue("http_api_uri_1"), }), RotateWhenChanged: types.MapNull(types.StringType), + Roles: types.SetNull(types.StringType), }, &rabbitmq.CredentialsResponse{ Id: "cid", @@ -192,6 +197,7 @@ func TestMapFields(t *testing.T) { types.StringValue("uri_1"), }), Username: types.StringValue("username"), + Roles: types.SetNull(types.StringType), RotateWhenChanged: types.MapNull(types.StringType), }, true, @@ -202,6 +208,7 @@ func TestMapFields(t *testing.T) { InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), RotateWhenChanged: types.MapNull(types.StringType), + Roles: types.SetNull(types.StringType), }, &rabbitmq.CredentialsResponse{ Id: "cid", @@ -236,6 +243,50 @@ func TestMapFields(t *testing.T) { Uri: types.StringNull(), Uris: types.ListValueMust(types.StringType, []attr.Value{}), Username: types.StringValue(""), + Roles: types.SetNull(types.StringType), + RotateWhenChanged: types.MapNull(types.StringType), + }, + true, + }, + { + "with_roles", + Model{ + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + RotateWhenChanged: types.MapNull(types.StringType), + Roles: types.SetValueMust(types.StringType, []attr.Value{ + types.StringValue("monitoring"), + }), + }, + &rabbitmq.CredentialsResponse{ + Id: "cid", + Raw: &rabbitmq.RawCredentials{ + Credentials: rabbitmq.Credentials{ + Host: "host", + Password: "password", + Username: "username", + }, + }, + }, + Model{ + Id: types.StringValue(fmt.Sprintf("pid,%s,iid,cid", testRegion)), + CredentialId: types.StringValue("cid"), + InstanceId: types.StringValue("iid"), + ProjectId: types.StringValue("pid"), + Region: types.StringValue(testRegion), + Host: types.StringValue("host"), + Hosts: types.ListNull(types.StringType), + HttpAPIURI: types.StringNull(), + HttpAPIURIs: types.ListNull(types.StringType), + Management: types.StringNull(), + Password: types.StringValue("password"), + Port: types.Int32Null(), + Uri: types.StringNull(), + Uris: types.ListNull(types.StringType), + Username: types.StringValue("username"), + Roles: types.SetValueMust(types.StringType, []attr.Value{ + types.StringValue("monitoring"), + }), RotateWhenChanged: types.MapNull(types.StringType), }, true, @@ -246,6 +297,7 @@ func TestMapFields(t *testing.T) { InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), RotateWhenChanged: types.MapNull(types.StringType), + Roles: types.SetNull(types.StringType), }, nil, Model{}, @@ -257,6 +309,7 @@ func TestMapFields(t *testing.T) { InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), RotateWhenChanged: types.MapNull(types.StringType), + Roles: types.SetNull(types.StringType), }, &rabbitmq.CredentialsResponse{}, Model{}, @@ -268,6 +321,7 @@ func TestMapFields(t *testing.T) { InstanceId: types.StringValue("iid"), ProjectId: types.StringValue("pid"), RotateWhenChanged: types.MapNull(types.StringType), + Roles: types.SetNull(types.StringType), }, &rabbitmq.CredentialsResponse{ Id: "cid", diff --git a/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go b/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go index e90af38bc..7e4de8c35 100644 --- a/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go +++ b/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go @@ -81,6 +81,7 @@ func resourceConfigCredential() string { resource "stackit_rabbitmq_credential" "credential" { project_id = stackit_rabbitmq_instance.instance.project_id instance_id = stackit_rabbitmq_instance.instance.instance_id + roles = ["monitoring"] } ` } @@ -148,6 +149,8 @@ func TestAccRabbitMQResource(t *testing.T) { ), resource.TestCheckResourceAttrSet("stackit_rabbitmq_credential.credential", "credential_id"), resource.TestCheckResourceAttrSet("stackit_rabbitmq_credential.credential", "host"), + resource.TestCheckResourceAttr("stackit_rabbitmq_credential.credential", "roles.#", "1"), + resource.TestCheckResourceAttr("stackit_rabbitmq_credential.credential", "roles.0", "monitoring"), ), }, // data source @@ -230,8 +233,9 @@ func TestAccRabbitMQResource(t *testing.T) { } return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, region, instanceId, credentialId), nil }, - ImportState: true, - ImportStateVerify: true, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"roles"}, }, // Update {