From 0bda18245f7bb7d13a4ae23917fb324c1a391d37 Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff Date: Fri, 22 May 2026 11:43:10 +0200 Subject: [PATCH] feat(object-storage): add rotate_when_changed attribute Signed-off-by: Mauritz Uphoff --- docs/resources/objectstorage_credential.md | 15 +++++++++++++++ .../resource.tf | 14 ++++++++++++++ .../objectstorage/credential/resource.go | 19 +++++++++++++++++++ .../objectstorage/credential/resource_test.go | 18 ++++++++++++++++-- 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/docs/resources/objectstorage_credential.md b/docs/resources/objectstorage_credential.md index 037c4a780..c5a47d098 100644 --- a/docs/resources/objectstorage_credential.md +++ b/docs/resources/objectstorage_credential.md @@ -19,6 +19,20 @@ resource "stackit_objectstorage_credential" "example" { expiration_timestamp = "2027-01-02T03:04:05Z" } +resource "time_rotating" "rotate" { + rotation_days = 80 +} + +resource "stackit_objectstorage_credential" "rotate_example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + credentials_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + expiration_timestamp = "2027-01-02T03:04:05Z" + + rotate_when_changed = { + rotation = time_rotating.rotate.id + } +} + # Only use the import statement, if you want to import an existing objectstorage credential import { to = stackit_objectstorage_credential.import-example @@ -38,6 +52,7 @@ import { - `expiration_timestamp` (String) Expiration timestamp, in RFC339 format without fractional seconds. Example: "2025-01-01T00:00:00Z". If not set, the credential never expires. - `region` (String) The resource region. If not defined, the provider region is used. +- `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_objectstorage_credential/resource.tf b/examples/resources/stackit_objectstorage_credential/resource.tf index 46e11717f..e011b0d60 100644 --- a/examples/resources/stackit_objectstorage_credential/resource.tf +++ b/examples/resources/stackit_objectstorage_credential/resource.tf @@ -4,6 +4,20 @@ resource "stackit_objectstorage_credential" "example" { expiration_timestamp = "2027-01-02T03:04:05Z" } +resource "time_rotating" "rotate" { + rotation_days = 80 +} + +resource "stackit_objectstorage_credential" "rotate_example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + credentials_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + expiration_timestamp = "2027-01-02T03:04:05Z" + + rotate_when_changed = { + rotation = time_rotating.rotate.id + } +} + # Only use the import statement, if you want to import an existing objectstorage credential import { to = stackit_objectstorage_credential.import-example diff --git a/stackit/internal/services/objectstorage/credential/resource.go b/stackit/internal/services/objectstorage/credential/resource.go index ec491e86f..cd57d4c9c 100644 --- a/stackit/internal/services/objectstorage/credential/resource.go +++ b/stackit/internal/services/objectstorage/credential/resource.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" + objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -46,6 +48,11 @@ type Model struct { SecretAccessKey types.String `tfsdk:"secret_access_key"` ExpirationTimestamp types.String `tfsdk:"expiration_timestamp"` Region types.String `tfsdk:"region"` + // 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 + // resource to be created. + RotateWhenChanged types.Map `tfsdk:"rotate_when_changed"` } // NewCredentialResource is a helper function to simplify the provider implementation. @@ -238,6 +245,18 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, stringplanmodifier.RequiresReplace(), }, }, + "rotate_when_changed": schema.MapAttribute{ + Description: "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.", + Optional: true, + Required: false, + ElementType: types.StringType, + PlanModifiers: []planmodifier.Map{ + mapplanmodifier.RequiresReplace(), + }, + }, }, } } diff --git a/stackit/internal/services/objectstorage/credential/resource_test.go b/stackit/internal/services/objectstorage/credential/resource_test.go index e8a2f44e4..6d55d8f1f 100644 --- a/stackit/internal/services/objectstorage/credential/resource_test.go +++ b/stackit/internal/services/objectstorage/credential/resource_test.go @@ -54,6 +54,7 @@ func TestMapFields(t *testing.T) { SecretAccessKey: types.StringValue(""), ExpirationTimestamp: types.StringNull(), Region: types.StringValue("eu01"), + RotateWhenChanged: types.MapNull(types.StringType), }, true, }, @@ -75,6 +76,7 @@ func TestMapFields(t *testing.T) { SecretAccessKey: types.StringValue("secret-key"), ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)), Region: types.StringValue("eu01"), + RotateWhenChanged: types.MapNull(types.StringType), }, true, }, @@ -95,6 +97,7 @@ func TestMapFields(t *testing.T) { SecretAccessKey: types.StringValue(""), ExpirationTimestamp: types.StringNull(), Region: types.StringValue("eu01"), + RotateWhenChanged: types.MapNull(types.StringType), }, true, }, @@ -113,6 +116,7 @@ func TestMapFields(t *testing.T) { SecretAccessKey: types.StringValue(""), ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)), Region: types.StringValue("eu01"), + RotateWhenChanged: types.MapNull(types.StringType), }, true, }, @@ -137,6 +141,7 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, CredentialsGroupId: tt.expected.CredentialsGroupId, CredentialId: tt.expected.CredentialId, + RotateWhenChanged: types.MapNull(types.StringType), } err := mapFields(tt.input, model, "eu01") if !tt.isValid && err == nil { @@ -175,6 +180,7 @@ func TestEnableProject(t *testing.T) { AccessKey: types.StringNull(), SecretAccessKey: types.StringNull(), ExpirationTimestamp: types.StringNull(), + RotateWhenChanged: types.MapNull(types.StringType), }, false, true, @@ -190,6 +196,7 @@ func TestEnableProject(t *testing.T) { AccessKey: types.StringNull(), SecretAccessKey: types.StringNull(), ExpirationTimestamp: types.StringNull(), + RotateWhenChanged: types.MapNull(types.StringType), }, true, false, @@ -205,6 +212,7 @@ func TestEnableProject(t *testing.T) { ProjectId: tt.expected.ProjectId, CredentialsGroupId: tt.expected.CredentialsGroupId, CredentialId: tt.expected.CredentialId, + RotateWhenChanged: types.MapNull(types.StringType), } err := enableProject(context.Background(), model, "eu01", client) if !tt.isValid && err == nil { @@ -255,6 +263,7 @@ func TestReadCredentials(t *testing.T) { SecretAccessKey: types.StringNull(), ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)), Region: types.StringValue("eu01"), + RotateWhenChanged: types.MapNull(types.StringType), }, true, false, @@ -291,6 +300,7 @@ func TestReadCredentials(t *testing.T) { SecretAccessKey: types.StringNull(), ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)), Region: types.StringValue("eu01"), + RotateWhenChanged: types.MapNull(types.StringType), }, true, false, @@ -327,6 +337,7 @@ func TestReadCredentials(t *testing.T) { SecretAccessKey: types.StringNull(), ExpirationTimestamp: types.StringValue(now.Format(time.RFC3339)), Region: types.StringValue("eu01"), + RotateWhenChanged: types.MapNull(types.StringType), }, true, false, @@ -338,7 +349,8 @@ func TestReadCredentials(t *testing.T) { AccessKeys: []objectstorage.AccessKey{}, }, Model{ - Region: types.StringValue("eu01"), + Region: types.StringValue("eu01"), + RotateWhenChanged: types.MapNull(types.StringType), }, false, false, @@ -369,7 +381,8 @@ func TestReadCredentials(t *testing.T) { }, }, Model{ - Region: types.StringValue("eu01"), + Region: types.StringValue("eu01"), + RotateWhenChanged: types.MapNull(types.StringType), }, false, false, @@ -431,6 +444,7 @@ func TestReadCredentials(t *testing.T) { ProjectId: tt.expectedModel.ProjectId, CredentialsGroupId: tt.expectedModel.CredentialsGroupId, CredentialId: tt.expectedModel.CredentialId, + RotateWhenChanged: types.MapNull(types.StringType), } found, err := readCredentials(context.Background(), model, "eu01", client) if !tt.isValid && err == nil {