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" {