diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e4e7daf..8bac729b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,8 @@ repos: rev: v1.96.1 hooks: - id: terraform_fmt + args: + - --args=-recursive - id: terraform_wrapper_module_for_each - id: terraform_docs args: diff --git a/README.md b/README.md index b6eba6c0..e161ef65 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ No modules. | [inventory\_self\_source\_destination](#input\_inventory\_self\_source\_destination) | Whether or not the inventory source bucket is also the destination bucket. | `bool` | `false` | no | | [inventory\_source\_account\_id](#input\_inventory\_source\_account\_id) | The inventory source account id. | `string` | `null` | no | | [inventory\_source\_bucket\_arn](#input\_inventory\_source\_bucket\_arn) | The inventory source bucket ARN. | `string` | `null` | no | -| [lifecycle\_rule](#input\_lifecycle\_rule) | List of maps containing configuration of object lifecycle management. | `any` | `[]` | no | +| [lifecycle\_rule](#input\_lifecycle\_rule) | List of maps containing configuration of object lifecycle management. Each lifecycle rule must contain 'id' and either 'enabled' or 'status', and may contain: 'filter', 'abort\_incomplete\_multipart\_upload\_days', 'expiration', 'transition', 'noncurrent\_version\_expiration', or 'noncurrent\_version\_transition'. | `any` | `[]` | no | | [logging](#input\_logging) | Map containing access bucket logging configuration. | `any` | `{}` | no | | [metric\_configuration](#input\_metric\_configuration) | Map containing bucket metric configuration. | `any` | `[]` | no | | [object\_lock\_configuration](#input\_object\_lock\_configuration) | Map containing S3 object locking configuration. | `any` | `{}` | no | diff --git a/tests/test_lifecycle_rules.tftest.hcl b/tests/test_lifecycle_rules.tftest.hcl new file mode 100644 index 00000000..ffc29c4a --- /dev/null +++ b/tests/test_lifecycle_rules.tftest.hcl @@ -0,0 +1,151 @@ +# Default AWS provider configuration +mock_provider "aws" { +} + +# Default required test variables +variables { + bucket_name = "test-bucket" + kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012" + readonly_iam_role_arns = [] + readwrite_iam_role_arns = [] + backup_enabled = false +} + +# Test 1 +run "verify_valid_lifecycle_rules" { + command = plan + + variables { + lifecycle_rules = [ + { + id = "log" + enabled = true + filter = { + tags = { + some = "value" + another = "value2" + } + } + transition = [ + { + days = 30 + storage_class = "ONEZONE_IA" + }, + { + days = 60 + storage_class = "GLACIER" + } + ] + }, + { + id = "log1" + enabled = true + abort_incomplete_multipart_upload_days = 7 + noncurrent_version_transition = [ + { + days = 30 + storage_class = "STANDARD_IA" + } + ] + noncurrent_version_expiration = { + days = 300 + } + }, + { + id = "expire_all_objects" + status = "Enabled" + expiration = { + days = 7 + } + noncurrent_version_expiration = { + noncurrent_days = 3 + } + abort_incomplete_multipart_upload_days = 1 + } + ] + } + + assert { + condition = length(var.lifecycle_rules) == 3 + error_message = "Expected 3 lifecycle rules" + } + + assert { + condition = alltrue([ + for rule in var.lifecycle_rules : contains(keys(rule), "id") + ]) + error_message = "All rules must have an id" + } + + assert { + condition = alltrue([ + for rule in var.lifecycle_rules : + anytrue([contains(keys(rule), "enabled"), contains(keys(rule), "status")]) + ]) + error_message = "All rules must have either enabled or status field" + } + + assert { + condition = alltrue([ + for rule in var.lifecycle_rules : + anytrue([ + !contains(keys(rule), "abort_incomplete_multipart_upload_days"), + can(tonumber(rule.abort_incomplete_multipart_upload_days)) + ]) + ]) + error_message = "abort_incomplete_multipart_upload_days must be a number" + } +} + +# Test 2 +run "fail_invalid_lifecycle_rules" { + command = plan + + variables { + lifecycle_rules = [ + { + id = "log1" + enabled = true + abort_incomplete_multipart_upload = { + days_after_initiation = "1" + } + noncurrent_version_transition = [ + { + days = 30 + storage_class = "STANDARD_IA" + } + ] + noncurrent_version_expiration = { + days = 300 + } + } + ] + } + + expect_failures = [ + var.lifecycle_rules + ] + + assert { + condition = !alltrue([ + for rule in var.lifecycle_rules : ( + contains(keys(rule), "id") && + (contains(keys(rule), "enabled") || contains(keys(rule), "status")) && + alltrue([ + for key in keys(rule) : contains([ + "id", + "enabled", + "status", + "filter", + "abort_incomplete_multipart_upload_days", + "expiration", + "transition", + "noncurrent_version_expiration", + "noncurrent_version_transition" + ], key) + ]) + ) + ]) + error_message = "Each lifecycle rule must contain 'id' and either 'enabled' or 'status', and may contain: 'filter', 'abort_incomplete_multipart_upload_days', 'expiration', 'transition', 'noncurrent_version_expiration', or 'noncurrent_version_transition'." + } +} diff --git a/variables.tf b/variables.tf index 92feec90..614c3cc2 100644 --- a/variables.tf +++ b/variables.tf @@ -191,9 +191,32 @@ variable "transition_default_minimum_object_size" { } variable "lifecycle_rule" { - description = "List of maps containing configuration of object lifecycle management." + description = "List of maps containing configuration of object lifecycle management. Each lifecycle rule must contain 'id' and either 'enabled' or 'status', and may contain: 'filter', 'abort_incomplete_multipart_upload_days', 'expiration', 'transition', 'noncurrent_version_expiration', or 'noncurrent_version_transition'." type = any default = [] + + validation { + condition = alltrue([ + for rule in var.lifecycle_rule : ( + contains(keys(rule), "id") && + (contains(keys(rule), "enabled") || contains(keys(rule), "status")) && + alltrue([ + for key in keys(rule) : contains([ + "id", + "enabled", + "status", + "filter", + "abort_incomplete_multipart_upload_days", + "expiration", + "transition", + "noncurrent_version_expiration", + "noncurrent_version_transition" + ], key) + ]) + ) + ]) + error_message = "Each lifecycle rule must contain 'id' and either 'enabled' or 'status', and may contain: 'filter', 'abort_incomplete_multipart_upload_days', 'expiration', 'transition', 'noncurrent_version_expiration', or 'noncurrent_version_transition'." + } } variable "replication_configuration" {