diff --git a/main.tf b/main.tf index 11de83b..9767bde 100644 --- a/main.tf +++ b/main.tf @@ -20,3 +20,12 @@ resource "aws_ecr_repository" "this" { } ) } + +resource "aws_ecr_lifecycle_policy" "this" { + count = length(var.lifecycle_policy.rules) > 0 ? 1 : 0 + + repository = aws_ecr_repository.this.name + policy = jsonencode({ + "rules" : var.lifecycle_policy.rules + }) +} diff --git a/tests/lifecycle_policy_test.go b/tests/lifecycle_policy_test.go new file mode 100644 index 0000000..9e80453 --- /dev/null +++ b/tests/lifecycle_policy_test.go @@ -0,0 +1,85 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/gruntwork-io/terratest/modules/aws" + "github.com/gruntwork-io/terratest/modules/random" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +func TestCanCreateRepositoryWithEmptyLifecyclePolicy(t *testing.T) { + t.Parallel() + + expectedName := fmt.Sprintf("test-%d", random.Random(0, 10)) + + region := aws.GetRandomRegion(t, nil, nil) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../", + Vars: map[string]interface{}{ + "name": expectedName, + }, + EnvVars: map[string]string{ + "AWS_DEFAULT_REGION": region, + }, + }) + + defer terraform.Destroy(t, terraformOptions) + + terraform.InitAndApply(t, terraformOptions) + + repository := aws.GetECRRepo(t, region, expectedName) + + assert.Equal(t, "AES256", *repository.EncryptionConfiguration.EncryptionType) + assert.Equal(t, "IMMUTABLE", *repository.ImageTagMutability) + assert.True(t, *repository.ImageScanningConfiguration.ScanOnPush) +} + +func TestCanCreateRepositoryWithLifecyclePolicyAndTaggedStatus(t *testing.T) { + t.Parallel() + + expectedName := fmt.Sprintf("test-%d", random.Random(0, 10)) + + region := aws.GetRandomRegion(t, nil, nil) + + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: "../", + Vars: map[string]interface{}{ + "name": expectedName, + "lifecycle_policy": map[string]interface{}{ + "rules": []map[string]interface{}{ + { + "rulePriority": 1, + "description": "Expire images older than 14 days", + "selection": map[string]interface{}{ + "tagStatus": "tagged", + "countType": "sinceImagePushed", + "countUnit": "days", + "countNumber": 14, + "tagPrefixList": []string{"test-repo"}, + }, + "action": map[string]interface{}{ + "type": "expire", + }, + }, + }, + }, + }, + EnvVars: map[string]string{ + "AWS_DEFAULT_REGION": region, + }, + }) + + defer terraform.Destroy(t, terraformOptions) + + terraform.InitAndApply(t, terraformOptions) + + repository := aws.GetECRRepo(t, region, expectedName) + + assert.Equal(t, "AES256", *repository.EncryptionConfiguration.EncryptionType) + assert.Equal(t, "IMMUTABLE", *repository.ImageTagMutability) + assert.True(t, *repository.ImageScanningConfiguration.ScanOnPush) +} diff --git a/variables.tf b/variables.tf index 76b36a8..b759d4e 100644 --- a/variables.tf +++ b/variables.tf @@ -46,6 +46,93 @@ variable "scan_on_push" { description = "Indicates whether images are scanned after being pushed to the repository (true) or not scanned (false). Defauls to true" } +## Lifecycle policy +variable "lifecycle_policy" { + type = object({ + rules = list(object({ + rulePriority = number + description = optional(string) + selection = object({ + tagStatus = string + countType = string + countNumber = number + countUnit = string + tagPrefixList = optional(list(string)) + }), + action = object({ + type = string + }) + })) + }) + + default = { + rules = [] + } + + description = "(Optional) A lifecycle policy for the repository. Default is empty" + + validation { + condition = alltrue([ + for rule in var.lifecycle_policy.rules : contains( + ["tagged", "untagged", "any"], + rule.selection.tagStatus + ) + ]) + + error_message = "The tag status must be one of: tagged, untagged or any." + } + + validation { + condition = alltrue([ + for rule in var.lifecycle_policy.rules : contains( + ["sinceImagePushed", "sinceImageCreated"], + rule.selection.countType + ) + ]) + + error_message = "The count type must be one of: sinceImagePushed or sinceImageCreated." + } + + validation { + condition = alltrue([ + for rule in var.lifecycle_policy.rules : contains( + ["days", "weeks", "months"], + rule.selection.countUnit + ) + ]) + + error_message = "The count unit must be one of: days, weeks or months." + } + + validation { + condition = alltrue([ + for rule in var.lifecycle_policy.rules : rule.selection.countNumber > 0 + ]) + + error_message = "The count number must be greater than 0." + } + + validation { + condition = alltrue([ + for rule in var.lifecycle_policy.rules : rule.rulePriority > 0 + ]) + + error_message = "The priority must be greater than 0." + } + + validation { + condition = alltrue([ + for rule in var.lifecycle_policy.rules : contains( + ["expire"], + rule.action.type + ) + ]) + + error_message = "The action type must be one of: expire." + } +} + +## Tags variable "tags" { type = map(string) default = {}