From fd7235afb3eab6d07ff3fc0ba64c7c10a86821b6 Mon Sep 17 00:00:00 2001 From: Marc Watts Date: Tue, 11 Jun 2024 13:40:16 +0100 Subject: [PATCH] Move to single aws_resource_tags rule --- rules/aws_resource_tags.go | 31 ++- rules/aws_resource_tags_test.go | 330 ++++++++++++++++++++++++-------- 2 files changed, 275 insertions(+), 86 deletions(-) diff --git a/rules/aws_resource_tags.go b/rules/aws_resource_tags.go index 17df2eb4..f4e73605 100644 --- a/rules/aws_resource_tags.go +++ b/rules/aws_resource_tags.go @@ -17,10 +17,11 @@ import ( ) const ( - defaultTagsBlockName = "default_tags" - tagsAttributeName = "tags" - tagBlockName = "tag" - providerAttributeName = "provider" + defaultTagsBlockName = "default_tags" + tagsAttributeName = "tags" + tagBlockName = "tag" + providerAttributeName = "provider" + autoScalingGroupResourceName = "aws_autoscaling_group" ) // AwsResourceTagsRule checks whether resources are tagged with valid values @@ -32,6 +33,7 @@ type awsResourceTagsRuleConfig struct { Required []string `hclext:"required,optional"` Values map[string][]string `hclext:"values,optional"` Exclude []string `hclext:"exclude,optional"` + Enabled bool `hclext:"enabled,optional"` } // awsAutoscalingGroupTag is used by go-cty to evaluate tags in aws_autoscaling_group resources @@ -283,7 +285,7 @@ func (r *AwsResourceTagsRule) checkAwsAutoScalingGroups(runner tflint.Runner, co func (r *AwsResourceTagsRule) checkAwsAutoScalingGroupsTag(runner tflint.Runner, resourceBlock *hclext.Block) (map[string]string, hcl.Range, error) { tags := map[string]string{} - resources, err := runner.GetResourceContent("aws_autoscaling_group", &hclext.BodySchema{ + resources, err := runner.GetResourceContent(autoScalingGroupResourceName, &hclext.BodySchema{ Blocks: []hclext.BlockSchema{ { Type: tagBlockName, @@ -335,7 +337,7 @@ func (r *AwsResourceTagsRule) checkAwsAutoScalingGroupsTag(runner tflint.Runner, func (r *AwsResourceTagsRule) checkAwsAutoScalingGroupsTags(runner tflint.Runner, resourceBlock *hclext.Block) (map[string]string, hcl.Range, error) { tags := map[string]string{} - resources, err := runner.GetResourceContent("aws_autoscaling_group", &hclext.BodySchema{ + resources, err := runner.GetResourceContent(autoScalingGroupResourceName, &hclext.BodySchema{ Attributes: []hclext.AttributeSchema{ {Name: tagsAttributeName}, }, @@ -382,19 +384,28 @@ func (r *AwsResourceTagsRule) emitIssue(runner tflint.Runner, tags map[string]st } tagsToMatch.Sort() - str := "" + errors := []string{} + + // Check the provided tags are valid for _, tagName := range tagsToMatch { allowedValues, ok := config.Values[tagName] // if the tag has a rule configuration then check if ok { valueProvided := tags[tagName] if !slices.Contains(allowedValues, valueProvided) { - str = str + fmt.Sprintf("Received '%s' for tag '%s', expected one of '%s'. ", valueProvided, tagName, strings.Join(allowedValues, ",")) + errors = append(errors, fmt.Sprintf("Received '%s' for tag '%s', expected one of '%s'.", valueProvided, tagName, strings.Join(allowedValues, ", "))) } } } - if len(str) > 0 { - runner.EmitIssue(r, strings.TrimSpace(str), location) + // Check all required tags are present + for _, requiredTagName := range config.Required { + if !stringInSlice(requiredTagName, tagsToMatch) { + errors = append(errors, fmt.Sprintf("Tag '%s' is required.", requiredTagName)) + } + } + + if len(errors) > 0 { + runner.EmitIssue(r, strings.Join(errors, " "), location) } } diff --git a/rules/aws_resource_tags_test.go b/rules/aws_resource_tags_test.go index 19e79dcf..5680c031 100644 --- a/rules/aws_resource_tags_test.go +++ b/rules/aws_resource_tags_test.go @@ -8,14 +8,14 @@ import ( "github.com/terraform-linters/tflint-plugin-sdk/helper" ) -// TODO: Test Required attr const testTagRule = ` rule "aws_resource_tags" { - enabled = true - values = { A: ["1", "foo"], B: ["2", "bar"] } + enabled = true + required = ["A", "B"] + values = { A: ["1", "foo"], B: ["2", "bar"] } } ` -func Test_AwsResourceInvalidTags(t *testing.T) { +func Test_AwsResourceTags(t *testing.T) { cases := []struct { Name string Content string @@ -23,18 +23,114 @@ func Test_AwsResourceInvalidTags(t *testing.T) { Expected helper.Issues RaiseErr error }{ + // basic assertions { - Name: "no tags assigned", + Name: "no tags assigned and no rules set", Content: ` - provider "aws" { region = "us-east-1" } + provider "aws" { } resource "aws_instance" "ec2_instance" { } resource "aws_instance" "ec2_instance" { }`, + Config: `rule "aws_resource_tags" { enabled = true }`, + Expected: helper.Issues{}, + }, + { + Name: "no tags assigned", + Content: ` + provider "aws" { } + resource "aws_instance" "ec2_instance" { }`, + Config: testTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceTagsRule(), + Message: "Tag 'A' is required. Tag 'B' is required.", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 3, Column: 4}, + End: hcl.Pos{Line: 3, Column: 42}, + }, + }, + }, + }, + { + Name: "no tags assigned and rule with only required set", + Content: ` + provider "aws" { } + resource "aws_instance" "ec2_instance" { }`, + Config: `rule "aws_resource_tags" { + required = ["A", "B"] + enabled = true + }`, + Expected: helper.Issues{ + { + Rule: NewAwsResourceTagsRule(), + Message: "Tag 'A' is required. Tag 'B' is required.", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 3, Column: 4}, + End: hcl.Pos{Line: 3, Column: 42}, + }, + }, + }, + }, + { + Name: "no tags assigned and rule with only values set", + Content: ` + provider "aws" { } + resource "aws_instance" "ec2_instance" { }`, + Config: `rule "aws_resource_tags" { + values = { A: ["1"], B: ["2"] } + enabled = true + }`, + Expected: helper.Issues{}, + }, + { + Name: "resource tag assignment with invalid values", + Content: ` + resource "aws_instance" "ec2_instance" { tags = { A = "0" } } + resource "aws_instance" "ec2_instance" { tags = { B = "0" } }`, + Config: testTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceTagsRule(), + Message: "Received '0' for tag 'A', expected one of '1, foo'. Tag 'B' is required.", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 2, Column: 52}, + End: hcl.Pos{Line: 2, Column: 63}, + }, + }, + { + Rule: NewAwsResourceTagsRule(), + Message: "Received '0' for tag 'B', expected one of '2, bar'. Tag 'A' is required.", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 3, Column: 52}, + End: hcl.Pos{Line: 3, Column: 63}, + }, + }, + }, + }, + { + Name: "resource tag assignment with valid values", + Content: `resource "aws_instance" "ec2_instance" { + tags = { A = "1", B = "bar" } + }`, + Config: testTagRule, + Expected: helper.Issues{}, + }, + { + Name: "resource tag assignment with unconfigured tag rule", + Content: `resource "aws_instance" "ec2_instance" { + tags = { A = "1", B = "bar", C = "3" } + }`, Config: testTagRule, Expected: helper.Issues{}, }, + // exclude { - Name: "resource with invalid explicit tags is excluded via rule", + Name: "resource with invalid tags is excluded via rule", Content: ` + provider "aws" { } resource "aws_instance" "ec2_instance_one" { tags = { A: "0", B: "0" } } resource "aws_instance" "ec2_instance_two" { tags = { A: "xar", B: "zar" } } resource "aws_s3_bucket" "s3_bucket_one" { tags = { A: "xar", B: "zar" } }`, @@ -47,28 +143,18 @@ func Test_AwsResourceInvalidTags(t *testing.T) { Expected: helper.Issues{ { Rule: NewAwsResourceTagsRule(), - Message: "Received 'xar' for tag 'A', expected one of '1,foo'. Received 'zar' for tag 'B', expected one of '2,bar'.", + Message: "Received 'xar' for tag 'A', expected one of '1, foo'. Received 'zar' for tag 'B', expected one of '2, bar'.", Range: hcl.Range{ Filename: "module.tf", - Start: hcl.Pos{Line: 4, Column: 54}, - End: hcl.Pos{Line: 4, Column: 76}, + Start: hcl.Pos{Line: 5, Column: 54}, + End: hcl.Pos{Line: 5, Column: 76}, }, }, }, }, + // assignment via variables { - Name: "valid provider tags assigned and no explicit tags assigned to resources", - Content: ` - provider "aws" { - default_tags { tags = { A = "1", B = "2" } } - } - resource "aws_instance" "ec2_instance_one" {} - resource "aws_instance" "ec2_instance_two" {}`, - Config: testTagRule, - Expected: helper.Issues{}, - }, - { - Name: "valid provider tags assigned with variables with no explicit tags assigned to resources", + Name: "valid provider tags assigned via variables", Content: ` variable "tags" { type = map(string, string) @@ -83,7 +169,7 @@ func Test_AwsResourceInvalidTags(t *testing.T) { Expected: helper.Issues{}, }, { - Name: "invalid provider tags assigned with variables with no explicit tags assigned to resources", + Name: "invalid provider tags assigned via variables", Content: ` variable "tags" { type = map(string, string) @@ -97,7 +183,7 @@ func Test_AwsResourceInvalidTags(t *testing.T) { Expected: helper.Issues{ { Rule: NewAwsResourceTagsRule(), - Message: "Received 'zar' for tag 'B', expected one of '2,bar'.", + Message: "Received 'zar' for tag 'B', expected one of '2, bar'.", Range: hcl.Range{ Filename: "module.tf", Start: hcl.Pos{Line: 9, Column: 4}, @@ -107,46 +193,58 @@ func Test_AwsResourceInvalidTags(t *testing.T) { }, }, { - Name: "valid provider tags assigned and invalid explicit tags assigned to resources", + Name: "invalid resource tags assigned with variables", Content: ` - provider "aws" { - default_tags { tags = { A = "1", B = "2" } } + variable "tags" { + type = map(string, string) + default = { A = "1", B = "zar" } } - resource "aws_instance" "ec2_instance_one" { tags = { A = "0" }}`, + provider "aws" { } + resource "aws_instance" "ec2_instance_one" { tags = var.tags }`, Config: testTagRule, Expected: helper.Issues{ { Rule: NewAwsResourceTagsRule(), - Message: "Received '0' for tag 'A', expected one of '1,foo'.", + Message: "Received 'zar' for tag 'B', expected one of '2, bar'.", Range: hcl.Range{ Filename: "module.tf", - Start: hcl.Pos{Line: 5, Column: 56}, - End: hcl.Pos{Line: 5, Column: 67}, + Start: hcl.Pos{Line: 7, Column: 56}, + End: hcl.Pos{Line: 7, Column: 64}, }, }, }, }, + // provider.default_tags { - Name: "valid default provider tags assigned and no tags assigned to autoscaling group", + Name: "valid provider tags assigned", Content: ` provider "aws" { default_tags { tags = { A = "1", B = "2" } } } - resource "aws_autoscaling_group" "asg" {}`, + resource "aws_instance" "ec2_instance_one" {} + resource "aws_instance" "ec2_instance_two" {}`, Config: testTagRule, Expected: helper.Issues{}, }, - // NOTE: This test surfaces the unknown relationship between Provider default tags - // and AutoScaling Groups tags. { - Name: "invalid default provider tags assigned and no tags assigned to autoscaling group", + Name: "valid provider tags assigned and invalid tags assigned to resources", Content: ` provider "aws" { - default_tags { tags = { A = "0", B = "0" } } + default_tags { tags = { A = "1", B = "2" } } } - resource "aws_autoscaling_group" "asg" {}`, - Config: testTagRule, - Expected: helper.Issues{}, + resource "aws_instance" "ec2_instance_one" { tags = { A = "0" }}`, + Config: testTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceTagsRule(), + Message: "Received '0' for tag 'A', expected one of '1, foo'.", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 5, Column: 56}, + End: hcl.Pos{Line: 5, Column: 67}, + }, + }, + }, }, { Name: "invalid default provider tags assigned and no tags assigned to resource", @@ -159,7 +257,7 @@ func Test_AwsResourceInvalidTags(t *testing.T) { Expected: helper.Issues{ { Rule: NewAwsResourceTagsRule(), - Message: "Received '0' for tag 'A', expected one of '1,foo'. Received 'foo' for tag 'B', expected one of '2,bar'.", + Message: "Received '0' for tag 'A', expected one of '1, foo'. Received 'foo' for tag 'B', expected one of '2, bar'.", Range: hcl.Range{ Filename: "module.tf", Start: hcl.Pos{Line: 5, Column: 4}, @@ -169,7 +267,7 @@ func Test_AwsResourceInvalidTags(t *testing.T) { }, }, { - Name: "multiple providers with explicit tags assigned and no custom tags assigned to resources", + Name: "multiple providers with default tags assigned", Content: ` provider "aws" { alias = "one" @@ -179,84 +277,125 @@ func Test_AwsResourceInvalidTags(t *testing.T) { alias = "two" default_tags { tags = { B = "2" } } } - resource "aws_instance" "ec2_instance" { provider = "one" } - resource "aws_instance" "ec2_instance" { provider = "two" }`, - Config: testTagRule, - Expected: helper.Issues{}, - }, - { - Name: "explicit resource tag assignment with invalid values", - Content: ` - resource "aws_instance" "ec2_instance" { tags = { A = "0" } } - resource "aws_instance" "ec2_instance" { tags = { B = "0" } }`, + resource "aws_instance" "ec2_instance" { provider = aws.one } + resource "aws_instance" "ec2_instance" { provider = aws.two }`, Config: testTagRule, Expected: helper.Issues{ { Rule: NewAwsResourceTagsRule(), - Message: "Received '0' for tag 'A', expected one of '1,foo'.", + Message: "Tag 'B' is required.", Range: hcl.Range{ Filename: "module.tf", - Start: hcl.Pos{Line: 2, Column: 52}, - End: hcl.Pos{Line: 2, Column: 63}, + Start: hcl.Pos{Line: 10, Column: 4}, + End: hcl.Pos{Line: 10, Column: 42}, }, }, { Rule: NewAwsResourceTagsRule(), - Message: "Received '0' for tag 'B', expected one of '2,bar'.", + Message: "Tag 'A' is required.", Range: hcl.Range{ Filename: "module.tf", - Start: hcl.Pos{Line: 3, Column: 52}, - End: hcl.Pos{Line: 3, Column: 63}, + Start: hcl.Pos{Line: 11, Column: 4}, + End: hcl.Pos{Line: 11, Column: 42}, }, }, }, }, + // aws_autoscaling_group { - Name: "explicit resource tag assignment with valid values", - Content: `resource "aws_instance" "ec2_instance" { tags = { A = "1", B = "bar" } }`, - Config: testTagRule, - Expected: helper.Issues{}, - }, - { - Name: "explicit resource tag assignment with unconfigured tag rule", - Content: `resource "aws_instance" "ec2_instance" { tags = { A = "1", B = "bar", C = "3" } }`, - Config: testTagRule, - Expected: helper.Issues{}, + Name: "autoscaling group with no tags", + Content: `resource "aws_autoscaling_group" "asg" {}`, + Config: testTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceTagsRule(), + Message: "Tag 'A' is required. Tag 'B' is required.", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 1, Column: 1}, + End: hcl.Pos{Line: 1, Column: 39}, + }, + }, + }, }, { - Name: "explicit autoscaling group resource with invalid tags", - Content: ` - resource "aws_autoscaling_group" "asg" { + Name: "autoscaling group with invalid tags assigned with block syntax", + Content: `resource "aws_autoscaling_group" "asg" { tag { key = "A" - value = "0" + value = "2" + propagate_at_launch = true } tag { key = "B" + value = "2" + propagate_at_launch = true + } + }`, + Config: testTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceTagsRule(), + Message: "Received '2' for tag 'A', expected one of '1, foo'.", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 1, Column: 1}, + End: hcl.Pos{Line: 1, Column: 39}, + }, + }, + }, + }, + { + Name: "autoscaling group with missing tags via block syntax", + Content: `resource "aws_autoscaling_group" "asg" { + tag { + key = "A" value = "foo" + propagate_at_launch = true } }`, Config: testTagRule, Expected: helper.Issues{ { Rule: NewAwsResourceTagsRule(), - Message: "Received '0' for tag 'A', expected one of '1,foo'. Received 'foo' for tag 'B', expected one of '2,bar'.", + Message: "Tag 'B' is required.", Range: hcl.Range{ Filename: "module.tf", - Start: hcl.Pos{Line: 2, Column: 4}, - End: hcl.Pos{Line: 2, Column: 42}, + Start: hcl.Pos{Line: 1, Column: 1}, + End: hcl.Pos{Line: 1, Column: 39}, + }, + }, + }, + }, + { + Name: "autoscaling group with missing tags", + Content: `resource "aws_autoscaling_group" "asg" { + tags = [ + { key = "A", value = "foo", propagate_at_launch = true } + ] + }`, + Config: testTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceTagsRule(), + Message: "Tag 'B' is required.", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 2, Column: 12}, + End: hcl.Pos{Line: 4, Column: 6}, }, }, }, }, { - Name: "autoscaling group with valid explicit tag assignment", + Name: "autoscaling group with valid tags assigned with block syntax", Content: ` resource "aws_autoscaling_group" "asg" { tag { key = "A" value = "foo" } + tag { key = "B" value = "bar" @@ -265,6 +404,45 @@ func Test_AwsResourceInvalidTags(t *testing.T) { Config: testTagRule, Expected: helper.Issues{}, }, + { + Name: "autoscaling group with valid tag assignment", + Content: ` + resource "aws_autoscaling_group" "asg" { + tags = [ + { key = "A", value = "foo", propagate_at_launch = true }, + { key = "B", value = "bar", propagate_at_launch = true } + ] + }`, + Config: testTagRule, + Expected: helper.Issues{}, + }, + { + Name: "autoscaling group with mixed tag assignment", + Content: ` + resource "aws_autoscaling_group" "asg" { + tag { + key = "A" + value = "foo" + } + + tags = [ + { key = "A", value = "foo", propagate_at_launch = true }, + { key = "B", value = "bar", propagate_at_launch = true } + ] + }`, + Config: testTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceTagsRule(), + Message: "Only tag block or tags attribute may be present, but found both", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 2, Column: 4}, + End: hcl.Pos{Line: 2, Column: 42}, + }, + }, + }, + }, } rule := NewAwsResourceTagsRule()