diff --git a/docs/data-sources/objectstorage_bucket.md b/docs/data-sources/objectstorage_bucket.md index bdf4fee22..e39f34d8f 100644 --- a/docs/data-sources/objectstorage_bucket.md +++ b/docs/data-sources/objectstorage_bucket.md @@ -34,5 +34,6 @@ data "stackit_objectstorage_bucket" "example" { ### Read-Only - `id` (String) Terraform's internal data source identifier. It is structured as "`project_id`,`region`,`name`". +- `object_lock` (Boolean) Enable Object Lock on this bucket. Can only be set at creation time. Requires an active project-level compliance lock. - `url_path_style` (String) - `url_virtual_hosted_style` (String) diff --git a/docs/resources/objectstorage_bucket.md b/docs/resources/objectstorage_bucket.md index 7abc9dbfa..d9af21c47 100644 --- a/docs/resources/objectstorage_bucket.md +++ b/docs/resources/objectstorage_bucket.md @@ -18,6 +18,19 @@ resource "stackit_objectstorage_bucket" "example" { name = "example-bucket" } +## With compliance lock +resource "stackit_objectstorage_compliance_lock" "example_with_lock" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_objectstorage_bucket" "example_with_lock" { + depends_on = [stackit_objectstorage_compliance_lock.example_with_lock] + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-bucket-with-lock" + object_lock = true +} + + # Only use the import statement, if you want to import an existing objectstorage bucket import { to = stackit_objectstorage_bucket.import-example @@ -35,6 +48,7 @@ import { ### Optional +- `object_lock` (Boolean) Enable Object Lock on this bucket. Can only be set at creation time. Requires an active project-level compliance lock. - `region` (String) The resource region. If not defined, the provider region is used. ### Read-Only diff --git a/examples/resources/stackit_objectstorage_bucket/resource.tf b/examples/resources/stackit_objectstorage_bucket/resource.tf index e8c1922cb..03e9989eb 100644 --- a/examples/resources/stackit_objectstorage_bucket/resource.tf +++ b/examples/resources/stackit_objectstorage_bucket/resource.tf @@ -3,6 +3,19 @@ resource "stackit_objectstorage_bucket" "example" { name = "example-bucket" } +## With compliance lock +resource "stackit_objectstorage_compliance_lock" "example_with_lock" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} + +resource "stackit_objectstorage_bucket" "example_with_lock" { + depends_on = [stackit_objectstorage_compliance_lock.example_with_lock] + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example-bucket-with-lock" + object_lock = true +} + + # Only use the import statement, if you want to import an existing objectstorage bucket import { to = stackit_objectstorage_bucket.import-example diff --git a/stackit/internal/services/objectstorage/bucket/datasource.go b/stackit/internal/services/objectstorage/bucket/datasource.go index 52a981b08..cd2f513b1 100644 --- a/stackit/internal/services/objectstorage/bucket/datasource.go +++ b/stackit/internal/services/objectstorage/bucket/datasource.go @@ -63,6 +63,7 @@ func (r *bucketDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, "id": "Terraform's internal data source identifier. It is structured as \"`project_id`,`region`,`name`\".", "name": "The bucket name. It must be DNS conform.", "project_id": "STACKIT Project ID to which the bucket is associated.", + "object_lock": "Enable Object Lock on this bucket. Can only be set at creation time. Requires an active project-level compliance lock.", "url_path_style": "URL in path style.", "url_virtual_hosted_style": "URL in virtual hosted style.", "region": "The resource region. If not defined, the provider region is used.", @@ -90,6 +91,10 @@ func (r *bucketDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, validate.NoSeparator(), }, }, + "object_lock": schema.BoolAttribute{ + Description: descriptions["object_lock"], + Computed: true, + }, "url_path_style": schema.StringAttribute{ Computed: true, }, diff --git a/stackit/internal/services/objectstorage/bucket/resource.go b/stackit/internal/services/objectstorage/bucket/resource.go index e6bd2eca9..1711c55cd 100644 --- a/stackit/internal/services/objectstorage/bucket/resource.go +++ b/stackit/internal/services/objectstorage/bucket/resource.go @@ -7,6 +7,8 @@ import ( "net/http" "strings" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" @@ -41,6 +43,7 @@ type Model struct { URLPathStyle types.String `tfsdk:"url_path_style"` URLVirtualHostedStyle types.String `tfsdk:"url_virtual_hosted_style"` Region types.String `tfsdk:"region"` + ObjectLock types.Bool `tfsdk:"object_lock"` } // NewBucketResource is a helper function to simplify the provider implementation. @@ -112,6 +115,7 @@ func (r *bucketResource) Schema(_ context.Context, _ resource.SchemaRequest, res "id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`name`\".", "name": "The bucket name. It must be DNS conform.", "project_id": "STACKIT Project ID to which the bucket is associated.", + "object_lock": "Enable Object Lock on this bucket. Can only be set at creation time. Requires an active project-level compliance lock.", "url_path_style": "URL in path style.", "url_virtual_hosted_style": "URL in virtual hosted style.", "region": "The resource region. If not defined, the provider region is used.", @@ -150,6 +154,15 @@ func (r *bucketResource) Schema(_ context.Context, _ resource.SchemaRequest, res validate.NoSeparator(), }, }, + "object_lock": schema.BoolAttribute{ + Description: descriptions["object_lock"], + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, "url_path_style": schema.StringAttribute{ Computed: true, }, @@ -196,7 +209,7 @@ func (r *bucketResource) Create(ctx context.Context, req resource.CreateRequest, } // Create new bucket - _, err = r.client.CreateBucket(ctx, projectId, region, bucketName).Execute() + _, err = r.client.CreateBucket(ctx, projectId, region, bucketName).ObjectLockEnabled(model.ObjectLock.ValueBool()).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Calling API: %v", err)) return @@ -367,6 +380,7 @@ func mapFields(bucketResp *objectstorage.GetBucketResponse, model *Model, region model.URLPathStyle = types.StringPointerValue(bucket.UrlPathStyle) model.URLVirtualHostedStyle = types.StringPointerValue(bucket.UrlVirtualHostedStyle) model.Region = types.StringValue(region) + model.ObjectLock = types.BoolPointerValue(bucket.ObjectLockEnabled) return nil } diff --git a/stackit/internal/services/objectstorage/bucket/resource_test.go b/stackit/internal/services/objectstorage/bucket/resource_test.go index 876e2fd57..18aaa46c0 100644 --- a/stackit/internal/services/objectstorage/bucket/resource_test.go +++ b/stackit/internal/services/objectstorage/bucket/resource_test.go @@ -56,12 +56,14 @@ func TestMapFields(t *testing.T) { Bucket: &objectstorage.Bucket{ UrlPathStyle: utils.Ptr("url/path/style"), UrlVirtualHostedStyle: utils.Ptr("url/virtual/hosted/style"), + ObjectLockEnabled: utils.Ptr(true), }, }, Model{ Id: types.StringValue(id), Name: types.StringValue("bname"), ProjectId: types.StringValue("pid"), + ObjectLock: types.BoolValue(true), URLPathStyle: types.StringValue("url/path/style"), URLVirtualHostedStyle: types.StringValue("url/virtual/hosted/style"), Region: types.StringValue("eu01"), diff --git a/stackit/internal/services/objectstorage/objectstorage_acc_test.go b/stackit/internal/services/objectstorage/objectstorage_acc_test.go index 9b69b105b..ff5e6dcd4 100644 --- a/stackit/internal/services/objectstorage/objectstorage_acc_test.go +++ b/stackit/internal/services/objectstorage/objectstorage_acc_test.go @@ -29,6 +29,9 @@ var testConfigVarsMin = config.Variables{ "objectstorage_bucket_name": config.StringVariable(fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(20, acctest.CharSetAlpha))), "objectstorage_credentials_group_name": config.StringVariable(fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(20, acctest.CharSetAlpha))), "expiration_timestamp": config.StringVariable(fmt.Sprintf("%d-01-02T03:04:05Z", time.Now().Year()+1)), + + "objectstorage_bucket_name_with_lock": config.StringVariable(fmt.Sprintf("tf-acc-test-%s", acctest.RandStringFromCharSet(20, acctest.CharSetAlpha))), + "object_lock": config.BoolVariable(true), } func TestAccObjectStorageResourceMin(t *testing.T) { @@ -46,6 +49,7 @@ func TestAccObjectStorageResourceMin(t *testing.T) { resource.TestCheckResourceAttr("stackit_objectstorage_bucket.bucket", "name", testutil.ConvertConfigVariable(testConfigVarsMin["objectstorage_bucket_name"])), resource.TestCheckResourceAttrSet("stackit_objectstorage_bucket.bucket", "url_path_style"), resource.TestCheckResourceAttrSet("stackit_objectstorage_bucket.bucket", "url_virtual_hosted_style"), + resource.TestCheckResourceAttr("stackit_objectstorage_bucket.bucket", "object_lock", "false"), // Credentials group data resource.TestCheckResourceAttr("stackit_objectstorage_credentials_group.credentials_group", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), @@ -85,6 +89,13 @@ func TestAccObjectStorageResourceMin(t *testing.T) { // compliance lock resource.TestCheckResourceAttr("stackit_objectstorage_compliance_lock.compliance_lock", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), resource.TestCheckResourceAttrSet("stackit_objectstorage_compliance_lock.compliance_lock", "max_retention_days"), + + // object storage with object lock enabled + resource.TestCheckResourceAttr("stackit_objectstorage_bucket.bucket_object_lock", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), + resource.TestCheckResourceAttr("stackit_objectstorage_bucket.bucket_object_lock", "name", testutil.ConvertConfigVariable(testConfigVarsMin["objectstorage_bucket_name_with_lock"])), + resource.TestCheckResourceAttrSet("stackit_objectstorage_bucket.bucket_object_lock", "url_path_style"), + resource.TestCheckResourceAttrSet("stackit_objectstorage_bucket.bucket_object_lock", "url_virtual_hosted_style"), + resource.TestCheckResourceAttr("stackit_objectstorage_bucket.bucket_object_lock", "object_lock", testutil.ConvertConfigVariable(testConfigVarsMin["object_lock"])), ), }, // Data source @@ -116,6 +127,10 @@ func TestAccObjectStorageResourceMin(t *testing.T) { } data "stackit_objectstorage_compliance_lock" "compliance_lock" { project_id = stackit_objectstorage_compliance_lock.compliance_lock.project_id + } + data "stackit_objectstorage_bucket" "bucket_object_lock" { + project_id = stackit_objectstorage_bucket.bucket_object_lock.project_id + name = stackit_objectstorage_bucket.bucket_object_lock.name }`, testutil.ObjectStorageProviderConfig()+resourceMinConfig, ), @@ -134,6 +149,10 @@ func TestAccObjectStorageResourceMin(t *testing.T) { "stackit_objectstorage_bucket.bucket", "url_virtual_hosted_style", "data.stackit_objectstorage_bucket.bucket", "url_virtual_hosted_style", ), + resource.TestCheckResourceAttrPair( + "stackit_objectstorage_bucket.bucket", "object_lock", + "data.stackit_objectstorage_bucket.bucket", "object_lock", + ), // Credentials group data resource.TestCheckResourceAttr("data.stackit_objectstorage_credentials_group.credentials_group", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), @@ -197,6 +216,25 @@ func TestAccObjectStorageResourceMin(t *testing.T) { // Compliance lock resource.TestCheckResourceAttr("data.stackit_objectstorage_compliance_lock.compliance_lock", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), resource.TestCheckResourceAttrSet("data.stackit_objectstorage_compliance_lock.compliance_lock", "max_retention_days"), + + // Bucket data with object lock + resource.TestCheckResourceAttr("data.stackit_objectstorage_bucket.bucket_object_lock", "project_id", testutil.ConvertConfigVariable(testConfigVarsMin["project_id"])), + resource.TestCheckResourceAttrPair( + "stackit_objectstorage_bucket.bucket_object_lock", "name", + "data.stackit_objectstorage_bucket.bucket_object_lock", "name", + ), + resource.TestCheckResourceAttrPair( + "stackit_objectstorage_bucket.bucket_object_lock", "url_path_style", + "data.stackit_objectstorage_bucket.bucket_object_lock", "url_path_style", + ), + resource.TestCheckResourceAttrPair( + "stackit_objectstorage_bucket.bucket_object_lock", "url_virtual_hosted_style", + "data.stackit_objectstorage_bucket.bucket_object_lock", "url_virtual_hosted_style", + ), + resource.TestCheckResourceAttrPair( + "stackit_objectstorage_bucket.bucket_object_lock", "object_lock", + "data.stackit_objectstorage_bucket.bucket_object_lock", "object_lock", + ), ), }, // Import diff --git a/stackit/internal/services/objectstorage/testfiles/resource-min.tf b/stackit/internal/services/objectstorage/testfiles/resource-min.tf index 1b05ee14f..cecb80b99 100644 --- a/stackit/internal/services/objectstorage/testfiles/resource-min.tf +++ b/stackit/internal/services/objectstorage/testfiles/resource-min.tf @@ -4,6 +4,9 @@ variable "objectstorage_bucket_name" {} variable "objectstorage_credentials_group_name" {} variable "expiration_timestamp" {} +variable "objectstorage_bucket_name_with_lock" {} +variable "object_lock" {} + resource "stackit_objectstorage_bucket" "bucket" { project_id = var.project_id name = var.objectstorage_bucket_name @@ -27,4 +30,11 @@ resource "stackit_objectstorage_credential" "credential_time" { resource "stackit_objectstorage_compliance_lock" "compliance_lock" { project_id = var.project_id +} + +resource "stackit_objectstorage_bucket" "bucket_object_lock" { + depends_on = [stackit_objectstorage_compliance_lock.compliance_lock] + project_id = var.project_id + name = var.objectstorage_bucket_name_with_lock + object_lock = var.object_lock } \ No newline at end of file