From b0f573b749bbfec731ec1c16eca48c459ad5090f Mon Sep 17 00:00:00 2001 From: Marc Watts Date: Tue, 7 May 2024 10:00:43 +0100 Subject: [PATCH 1/9] Add Resource Invalid Tags rule --- rules/aws_resource_invalid_tags.go | 380 ++++++++++++++++++++++++ rules/aws_resource_invalid_tags_test.go | 247 +++++++++++++++ rules/aws_resource_missing_tags.go | 25 -- rules/aws_resource_tags.go | 65 ++++ 4 files changed, 692 insertions(+), 25 deletions(-) create mode 100644 rules/aws_resource_invalid_tags.go create mode 100644 rules/aws_resource_invalid_tags_test.go create mode 100644 rules/aws_resource_tags.go diff --git a/rules/aws_resource_invalid_tags.go b/rules/aws_resource_invalid_tags.go new file mode 100644 index 00000000..c49b4b92 --- /dev/null +++ b/rules/aws_resource_invalid_tags.go @@ -0,0 +1,380 @@ +package rules + +import ( + "fmt" + "sort" + "strings" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/logger" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/terraform-linters/tflint-ruleset-aws/aws" + "github.com/terraform-linters/tflint-ruleset-aws/project" + "github.com/terraform-linters/tflint-ruleset-aws/rules/tags" + "github.com/zclconf/go-cty/cty" + "golang.org/x/exp/slices" +) + +// AwsResourceInvalidTagsRule checks whether resources are tagged with valid values +type AwsResourceInvalidTagsRule struct { + tflint.DefaultRule +} + +type awsResourceInvalidTagsRuleConfig struct { + Tags map[string][]string `hclext:"tags"` + Exclude []string `hclext:"exclude,optional"` +} + +// NewAwsResourceInvalidTagsRule returns new rules for all resources that support tags +func NewAwsResourceInvalidTagsRule() *AwsResourceInvalidTagsRule { + return &AwsResourceInvalidTagsRule{} +} + +// Name returns the rule name +func (r *AwsResourceInvalidTagsRule) Name() string { + return "aws_resource_invalid_tags" +} + +// Enabled returns whether the rule is enabled by default +func (r *AwsResourceInvalidTagsRule) Enabled() bool { + return false +} + +// Severity returns the rule severity +func (r *AwsResourceInvalidTagsRule) Severity() tflint.Severity { + return tflint.NOTICE +} + +// Link returns the rule reference link +func (r *AwsResourceInvalidTagsRule) Link() string { + return project.ReferenceLink(r.Name()) +} + +// Check checks resources for invalid tags +func (r *AwsResourceInvalidTagsRule) Check(runner tflint.Runner) error { + config := awsResourceInvalidTagsRuleConfig{} + if err := runner.DecodeRuleConfig(r.Name(), &config); err != nil { + return err + } + + providerTagsMap, err := r.getProviderLevelTags(runner) + + if err != nil { + return err + } + + for _, resourceType := range tags.Resources { + // Skip this resource if its type is excluded in configuration + if stringInSlice(resourceType, config.Exclude) { + continue + } + + resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: tagsAttributeName}, + {Name: providerAttributeName}, + }, + }, nil) + + if err != nil { + return err + } + + if resources.IsEmpty() { + continue + } + + for _, resource := range resources.Blocks { + providerAlias := "default" + + // Override the provider alias if defined + if val, ok := resource.Body.Attributes[providerAttributeName]; ok { + provider, diagnostics := aws.DecodeProviderConfigRef(val.Expr, "provider") + if diagnostics.HasErrors() { + logger.Error("error decoding provider: %w", diagnostics) + return diagnostics + } + providerAlias = provider.Alias + } + + providerAliasTags := providerTagsMap[providerAlias] + + // If the resource has a tags attribute + if attribute, okResource := resource.Body.Attributes[tagsAttributeName]; okResource { + logger.Debug( + "Walk `%s` attribute", + resource.Labels[0]+"."+resource.Labels[1]+"."+tagsAttributeName, + ) + + err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { + knownTags, known := getKnownForValue(val) + if !known { + logger.Warn("The invalid aws tags rule can only evaluate provided variables, skipping %s.", resource.Labels[0]+"."+resource.Labels[1]+"."+tagsAttributeName) + return nil + } + + // merge the known tags with the provider tags to comply with the implementation + // https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/resource-tagging#propagating-tags-to-all-resources + for providerTagKey, providerTagValue := range providerAliasTags { + if _, ok := knownTags[providerTagKey]; !ok { + knownTags[providerTagKey] = providerTagValue + } + } + + r.emitIssue(runner, knownTags, config, attribute.Expr.Range()) + return nil + }, nil) + + if err != nil { + return err + } + } else { + logger.Debug("Walk `%s` resource", resource.Labels[0]+"."+resource.Labels[1]) + r.emitIssue(runner, providerAliasTags, config, resource.DefRange) + } + } + } + + // Special handling for tags on aws_autoscaling_group resources + if err := r.checkAwsAutoScalingGroups(runner, config); err != nil { + return err + } + + return nil +} + +func (r *AwsResourceInvalidTagsRule) getProviderLevelTags(runner tflint.Runner) (awsProvidersTags, error) { + providerSchema := &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + { + Name: "alias", + Required: false, + }, + }, + Blocks: []hclext.BlockSchema{ + { + Type: defaultTagsBlockName, + Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: tagsAttributeName}}}, + }, + }, + } + + providerBody, err := runner.GetProviderContent("aws", providerSchema, nil) + if err != nil { + return nil, err + } + + // Get provider default tags + allProviderTags := make(awsProvidersTags) + var providerAlias string + + for _, provider := range providerBody.Blocks.OfType(providerAttributeName) { + // Get the alias attribute, in terraform when there is a single aws provider its called "default" + providerAttr, ok := provider.Body.Attributes["alias"] + if !ok { + providerAlias = "default" + } else { + err := runner.EvaluateExpr(providerAttr.Expr, func(alias string) error { + logger.Debug("Walk `%s` provider", providerAlias) + providerAlias = alias + // Init the provider reference even if it doesn't have tags + allProviderTags[alias] = nil + return nil + }, nil) + if err != nil { + return nil, err + } + } + + for _, block := range provider.Body.Blocks { + attr, ok := block.Body.Attributes[tagsAttributeName] + if !ok { + continue + } + + err := runner.EvaluateExpr(attr.Expr, func(val cty.Value) error { + tags, known := getKnownForValue(val) + + if !known { + logger.Warn("The invalid aws tags rule can only evaluate provided variables, skipping %s.", provider.Labels[0]+"."+providerAlias+"."+defaultTagsBlockName+"."+tagsAttributeName) + return nil + } + + logger.Debug("Walk `%s` provider with tags `%v`", providerAlias, tags) + allProviderTags[providerAlias] = tags + return nil + }, nil) + + if err != nil { + return nil, err + } + } + } + return allProviderTags, nil +} + +// checkAwsAutoScalingGroups handles the special case for tags on AutoScaling Groups +// See: https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/autoscaling_tags.go +func (r *AwsResourceInvalidTagsRule) checkAwsAutoScalingGroups(runner tflint.Runner, config awsResourceInvalidTagsRuleConfig) error { + resourceType := "aws_autoscaling_group" + + // Skip autoscaling group check if its type is excluded in configuration + if stringInSlice(resourceType, config.Exclude) { + return nil + } + + resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{}, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + asgTagBlockTags, tagBlockLocation, err := r.checkAwsAutoScalingGroupsTag(runner, resource) + if err != nil { + return err + } + + asgTagsAttributeTags, tagsAttributeLocation, err := r.checkAwsAutoScalingGroupsTags(runner, resource) + if err != nil { + return err + } + + switch { + case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) > 0: + runner.EmitIssue(r, "Only tag block or tags attribute may be present, but found both", resource.DefRange) + case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) == 0: + r.emitIssue(runner, map[string]string{}, config, resource.DefRange) + case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) == 0: + tags := asgTagBlockTags + location := tagBlockLocation + r.emitIssue(runner, tags, config, location) + case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) > 0: + tags := asgTagsAttributeTags + location := tagsAttributeLocation + r.emitIssue(runner, tags, config, location) + } + } + + return nil +} + +// checkAwsAutoScalingGroupsTag checks tag{} blocks on aws_autoscaling_group resources +func (r *AwsResourceInvalidTagsRule) 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{ + Blocks: []hclext.BlockSchema{ + { + Type: tagBlockName, + Body: &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: "key"}, + {Name: "value"}, + }, + }, + }, + }, + }, nil) + if err != nil { + return tags, hcl.Range{}, err + } + + for _, resource := range resources.Blocks { + if resource.Labels[0] != resourceBlock.Labels[0] { + continue + } + + for _, tag := range resource.Body.Blocks { + keyAttribute, keyExists := tag.Body.Attributes["key"] + if !keyExists { + return tags, hcl.Range{}, fmt.Errorf(`Did not find expected field "key" in aws_autoscaling_group "%s" starting at line %d`, resource.Labels[0], resource.DefRange.Start.Line) + } + + valueAttribute, valueExists := tag.Body.Attributes["value"] + if !valueExists { + return tags, hcl.Range{}, fmt.Errorf(`Did not find expected field "value" in aws_autoscaling_group "%s" starting at line %d`, resource.Labels[0], resource.DefRange.Start.Line) + } + + err := runner.EvaluateExpr(keyAttribute.Expr, func(key string) error { + return runner.EvaluateExpr(valueAttribute.Expr, func(value string) error { + tags[key] = value + return nil + }, nil) + }, nil) + if err != nil { + return tags, hcl.Range{}, err + } + } + } + + return tags, resourceBlock.DefRange, nil +} + +// checkAwsAutoScalingGroupsTag checks the tags attribute on aws_autoscaling_group resources +func (r *AwsResourceInvalidTagsRule) 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{ + Attributes: []hclext.AttributeSchema{ + {Name: tagsAttributeName}, + }, + }, nil) + if err != nil { + return tags, hcl.Range{}, err + } + + for _, resource := range resources.Blocks { + if resource.Labels[0] != resourceBlock.Labels[0] { + continue + } + + attribute, ok := resource.Body.Attributes[tagsAttributeName] + if ok { + wantType := cty.List(cty.Object(map[string]cty.Type{ + "key": cty.String, + "value": cty.String, + "propagate_at_launch": cty.Bool, + })) + err := runner.EvaluateExpr(attribute.Expr, func(asgTags []awsAutoscalingGroupTag) error { + for _, tag := range asgTags { + tags[tag.Key] = tag.Value + } + return nil + }, &tflint.EvaluateExprOption{WantType: &wantType}) + if err != nil { + return tags, attribute.Expr.Range(), err + } + return tags, attribute.Expr.Range(), nil + } + } + + return tags, resourceBlock.DefRange, nil +} + +func (r *AwsResourceInvalidTagsRule) emitIssue(runner tflint.Runner, tags map[string]string, config awsResourceInvalidTagsRuleConfig, location hcl.Range) { + // sort the tag names for deterministic output + // only evaluate the given tags on the resource NOT the configured tags + // the checking of tag presence should use the `aws_resource_missing_tags` lint rule + tagsToMatch := sort.StringSlice{} + for tagName := range tags { + tagsToMatch = append(tagsToMatch, tagName) + } + tagsToMatch.Sort() + + str := "" + for _, tagName := range tagsToMatch { + allowedValues, ok := config.Tags[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, ",")) + } + } + } + + if len(str) > 0 { + runner.EmitIssue(r, strings.TrimSpace(str), location) + } +} diff --git a/rules/aws_resource_invalid_tags_test.go b/rules/aws_resource_invalid_tags_test.go new file mode 100644 index 00000000..f763751b --- /dev/null +++ b/rules/aws_resource_invalid_tags_test.go @@ -0,0 +1,247 @@ +package rules + +import ( + "testing" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/stretchr/testify/assert" + "github.com/terraform-linters/tflint-plugin-sdk/helper" +) + +const testInvalidTagRule = ` +rule "aws_resource_invalid_tags" { + enabled = true + tags = { A: ["1", "foo"], B: ["2", "bar"] } +} ` + +func Test_AwsResourceInvalidTags(t *testing.T) { + cases := []struct { + Name string + Content string + Config string + Expected helper.Issues + RaiseErr error + }{ + { + Name: "no tags assigned", + Content: ` + provider "aws" { region = "us-east-1" } + resource "aws_instance" "ec2_instance" { } + resource "aws_instance" "ec2_instance" { }`, + Config: testInvalidTagRule, + Expected: helper.Issues{}, + }, + { + Name: "resource with invalid explicit tags is excluded via rule", + Content: ` + 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" } }`, + Config: ` + rule "aws_resource_invalid_tags" { + enabled = true + tags = { A: ["1", "foo"], B: ["2", "bar"] } + exclude = ["aws_instance"] + }`, + Expected: helper.Issues{ + { + Rule: NewAwsResourceInvalidTagsRule(), + 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}, + }, + }, + }, + }, + { + 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: testInvalidTagRule, + Expected: helper.Issues{}, + }, + { + Name: "valid provider tags assigned and invalid explicit tags assigned to resources", + Content: ` + provider "aws" { + default_tags { tags = { A = "1", B = "2" } } + } + resource "aws_instance" "ec2_instance_one" { tags = { A = "0" }}`, + Config: testInvalidTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceInvalidTagsRule(), + 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: "valid default provider tags assigned and no tags assigned to autoscaling group", + Content: ` + provider "aws" { + default_tags { tags = { A = "1", B = "2" } } + } + resource "aws_autoscaling_group" "asg" {}`, + Config: testInvalidTagRule, + 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", + Content: ` + provider "aws" { + default_tags { tags = { A = "0", B = "0" } } + } + resource "aws_autoscaling_group" "asg" {}`, + Config: testInvalidTagRule, + Expected: helper.Issues{}, + }, + { + Name: "invalid default provider tags assigned and no tags assigned to resource", + Content: ` + provider "aws" { + default_tags { tags = { A = "0", B = "foo" } } + } + resource "aws_s3_bucket" "bucket" {}`, + Config: testInvalidTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceInvalidTagsRule(), + 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}, + End: hcl.Pos{Line: 5, Column: 37}, + }, + }, + }, + }, + { + Name: "multiple providers with explicit tags assigned and no custom tags assigned to resources", + Content: ` + provider "aws" { + alias = "one" + default_tags { tags = { A = "1" } } + } + provider "aws" { + alias = "two" + default_tags { tags = { B = "2" } } + } + resource "aws_instance" "ec2_instance" { provider = "one" } + resource "aws_instance" "ec2_instance" { provider = "two" }`, + Config: testInvalidTagRule, + 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" } }`, + Config: testInvalidTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceInvalidTagsRule(), + Message: "Received '0' for tag 'A', expected one of '1,foo'.", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 2, Column: 52}, + End: hcl.Pos{Line: 2, Column: 63}, + }, + }, + { + Rule: NewAwsResourceInvalidTagsRule(), + Message: "Received '0' for tag 'B', expected one of '2,bar'.", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 3, Column: 52}, + End: hcl.Pos{Line: 3, Column: 63}, + }, + }, + }, + }, + { + Name: "explicit resource tag assignment with valid values", + Content: `resource "aws_instance" "ec2_instance" { tags = { A = "1", B = "bar" } }`, + Config: testInvalidTagRule, + 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: testInvalidTagRule, + Expected: helper.Issues{}, + }, + { + Name: "explicit autoscaling group resource with invalid tags", + Content: ` + resource "aws_autoscaling_group" "asg" { + tag { + key = "A" + value = "0" + } + tag { + key = "B" + value = "foo" + } + }`, + Config: testInvalidTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceInvalidTagsRule(), + 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: 2, Column: 4}, + End: hcl.Pos{Line: 2, Column: 42}, + }, + }, + }, + }, + { + Name: "autoscaling group with valid explicit tag assignment", + Content: ` + resource "aws_autoscaling_group" "asg" { + tag { + key = "A" + value = "foo" + } + tag { + key = "B" + value = "bar" + } + }`, + Config: testInvalidTagRule, + Expected: helper.Issues{}, + }, + } + + rule := NewAwsResourceInvalidTagsRule() + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + runner := helper.TestRunner(t, map[string]string{"module.tf": tc.Content, ".tflint.hcl": tc.Config}) + + err := rule.Check(runner) + + if tc.RaiseErr == nil && err != nil { + t.Fatalf("Unexpected error occurred in test \"%s\": %s", tc.Name, err) + } + + assert.Equal(t, tc.RaiseErr, err) + + helper.AssertIssues(t, tc.Expected, runner.Issues) + }) + } +} diff --git a/rules/aws_resource_missing_tags.go b/rules/aws_resource_missing_tags.go index dd6ceec2..f5ee513a 100644 --- a/rules/aws_resource_missing_tags.go +++ b/rules/aws_resource_missing_tags.go @@ -26,13 +26,6 @@ type awsResourceTagsRuleConfig struct { Exclude []string `hclext:"exclude,optional"` } -const ( - defaultTagsBlockName = "default_tags" - tagsAttributeName = "tags" - tagBlockName = "tag" - providerAttributeName = "provider" -) - // NewAwsResourceMissingTagsRule returns new rules for all resources that support tags func NewAwsResourceMissingTagsRule() *AwsResourceMissingTagsRule { return &AwsResourceMissingTagsRule{} @@ -211,15 +204,6 @@ func (r *AwsResourceMissingTagsRule) Check(runner tflint.Runner) error { return nil } -// awsAutoscalingGroupTag is used by go-cty to evaluate tags in aws_autoscaling_group resources -// The type does not need to be public, but its fields do -// https://github.com/zclconf/go-cty/blob/master/docs/gocty.md#converting-to-and-from-structs -type awsAutoscalingGroupTag struct { - Key string `cty:"key"` - Value string `cty:"value"` - PropagateAtLaunch bool `cty:"propagate_at_launch"` -} - // checkAwsAutoScalingGroups handles the special case for tags on AutoScaling Groups // See: https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/autoscaling_tags.go func (r *AwsResourceMissingTagsRule) checkAwsAutoScalingGroups(runner tflint.Runner, config awsResourceTagsRuleConfig) error { @@ -365,15 +349,6 @@ func (r *AwsResourceMissingTagsRule) emitIssue(runner tflint.Runner, tags []stri } } -func stringInSlice(a string, list []string) bool { - for _, b := range list { - if b == a { - return true - } - } - return false -} - // getKeysForValue returns a list of keys from a cty.Value, which is assumed to be a map (or unknown). // It returns a boolean indicating whether the keys were known. // If _any_ key is unknown, the entire value is considered unknown, since we can't know if a required tag might be matched by the unknown key. diff --git a/rules/aws_resource_tags.go b/rules/aws_resource_tags.go new file mode 100644 index 00000000..2ebc9048 --- /dev/null +++ b/rules/aws_resource_tags.go @@ -0,0 +1,65 @@ +package rules + +import ( + "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/zclconf/go-cty/cty" +) + +const ( + defaultTagsBlockName = "default_tags" + tagsAttributeName = "tags" + tagBlockName = "tag" + providerAttributeName = "provider" +) + +type AwsResourceTagsRule struct { + tflint.DefaultRule +} + +// awsAutoscalingGroupTag is used by go-cty to evaluate tags in aws_autoscaling_group resources +// The type does not need to be public, but its fields do +// https://github.com/zclconf/go-cty/blob/master/docs/gocty.md#converting-to-and-from-structs +type awsAutoscalingGroupTag struct { + Key string `cty:"key"` + Value string `cty:"value"` + PropagateAtLaunch bool `cty:"propagate_at_launch"` +} + +type awsTags map[string]string +type awsProvidersTags map[string]awsTags + +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +func getKnownForValue(value cty.Value) (map[string]string, bool) { + tags := map[string]string{} + + if !value.CanIterateElements() || !value.IsKnown() { + return nil, false + } + if value.IsNull() { + return tags, true + } + + return tags, !value.ForEachElement(func(key, value cty.Value) bool { + // If any key is unknown or sensitive, return early as any missing tag could be this unknown key. + if !key.IsKnown() || key.IsNull() || key.IsMarked() { + return true + } + + if !value.IsKnown() || value.IsMarked() { + return true + } + + // We assume the value of the tag is ALWAYS a string + tags[key.AsString()] = value.AsString() + + return false + }) +} From c1308934b58f96bd86c23cdbe0383806e4a35a34 Mon Sep 17 00:00:00 2001 From: Marc Watts Date: Tue, 7 May 2024 14:52:05 +0100 Subject: [PATCH 2/9] Add additional resources as-per README instructions --- docs/rules/aws_resource_invalid_tags.md | 46 +++++++++++++++++++++++++ rules/provider.go | 1 + 2 files changed, 47 insertions(+) create mode 100644 docs/rules/aws_resource_invalid_tags.md diff --git a/docs/rules/aws_resource_invalid_tags.md b/docs/rules/aws_resource_invalid_tags.md new file mode 100644 index 00000000..015a922a --- /dev/null +++ b/docs/rules/aws_resource_invalid_tags.md @@ -0,0 +1,46 @@ +# aws_resource_invalid_tags + +Require tags to be assigned to a specific set of values. + +## Example + +```hcl +rule "aws_resource_invalid_tags" { + enabled = true + tags = { + Department = ["finance", "hr", "payments", "engineering"] + Environment = ["sandbox", "staging", "production"] + } + exclude = ["aws_autoscaling_group"] +} + +provider "aws" { + ... + default_tags { + tags = { Environment = "sandbox" } + } +} + +resource "aws_s3_bucket" "bucket" { + ... + tags = { Project: "homepage", Department = "science" } +} +``` + +``` +$ tflint +1 issue(s) found: + +Notice: aws_s3_bucket.bucket Received 'science' for tag 'Department', expected one of 'finance,hr,payments,engineering'. + + on test.tf line 3: + 3: tags = { Project: "homepage", Department = "science" } +``` + +## Why + +Enforce standard tag values across all resources. + +## How To Fix + +Align the provider, resource or autoscaling group tags to the configured expectation. diff --git a/rules/provider.go b/rules/provider.go index 4d450b64..9570d2a7 100644 --- a/rules/provider.go +++ b/rules/provider.go @@ -40,6 +40,7 @@ var manualRules = []tflint.Rule{ NewAwsSecurityGroupInvalidProtocolRule(), NewAwsSecurityGroupRuleInvalidProtocolRule(), NewAwsProviderMissingDefaultTagsRule(), + NewAwsResourceInvalidTagsRule(), } // Rules is a list of all rules From 1632ca64abf4f53c7b71577d152a76ad7b1b11bc Mon Sep 17 00:00:00 2001 From: Marc Watts Date: Tue, 11 Jun 2024 09:02:44 +0100 Subject: [PATCH 3/9] [wip] Merge tag rules into single dsl --- rules/aws_resource_invalid_tags.go | 380 ------------ rules/aws_resource_missing_tags.go | 374 ------------ rules/aws_resource_missing_tags_test.go | 547 ------------------ rules/aws_resource_tags.go | 375 +++++++++++- ...tags_test.go => aws_resource_tags_test.go} | 88 ++- rules/provider.go | 3 +- rules/utils.go | 59 ++ 7 files changed, 479 insertions(+), 1347 deletions(-) delete mode 100644 rules/aws_resource_invalid_tags.go delete mode 100644 rules/aws_resource_missing_tags.go delete mode 100644 rules/aws_resource_missing_tags_test.go rename rules/{aws_resource_invalid_tags_test.go => aws_resource_tags_test.go} (76%) diff --git a/rules/aws_resource_invalid_tags.go b/rules/aws_resource_invalid_tags.go deleted file mode 100644 index c49b4b92..00000000 --- a/rules/aws_resource_invalid_tags.go +++ /dev/null @@ -1,380 +0,0 @@ -package rules - -import ( - "fmt" - "sort" - "strings" - - hcl "github.com/hashicorp/hcl/v2" - "github.com/terraform-linters/tflint-plugin-sdk/hclext" - "github.com/terraform-linters/tflint-plugin-sdk/logger" - "github.com/terraform-linters/tflint-plugin-sdk/tflint" - "github.com/terraform-linters/tflint-ruleset-aws/aws" - "github.com/terraform-linters/tflint-ruleset-aws/project" - "github.com/terraform-linters/tflint-ruleset-aws/rules/tags" - "github.com/zclconf/go-cty/cty" - "golang.org/x/exp/slices" -) - -// AwsResourceInvalidTagsRule checks whether resources are tagged with valid values -type AwsResourceInvalidTagsRule struct { - tflint.DefaultRule -} - -type awsResourceInvalidTagsRuleConfig struct { - Tags map[string][]string `hclext:"tags"` - Exclude []string `hclext:"exclude,optional"` -} - -// NewAwsResourceInvalidTagsRule returns new rules for all resources that support tags -func NewAwsResourceInvalidTagsRule() *AwsResourceInvalidTagsRule { - return &AwsResourceInvalidTagsRule{} -} - -// Name returns the rule name -func (r *AwsResourceInvalidTagsRule) Name() string { - return "aws_resource_invalid_tags" -} - -// Enabled returns whether the rule is enabled by default -func (r *AwsResourceInvalidTagsRule) Enabled() bool { - return false -} - -// Severity returns the rule severity -func (r *AwsResourceInvalidTagsRule) Severity() tflint.Severity { - return tflint.NOTICE -} - -// Link returns the rule reference link -func (r *AwsResourceInvalidTagsRule) Link() string { - return project.ReferenceLink(r.Name()) -} - -// Check checks resources for invalid tags -func (r *AwsResourceInvalidTagsRule) Check(runner tflint.Runner) error { - config := awsResourceInvalidTagsRuleConfig{} - if err := runner.DecodeRuleConfig(r.Name(), &config); err != nil { - return err - } - - providerTagsMap, err := r.getProviderLevelTags(runner) - - if err != nil { - return err - } - - for _, resourceType := range tags.Resources { - // Skip this resource if its type is excluded in configuration - if stringInSlice(resourceType, config.Exclude) { - continue - } - - resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: tagsAttributeName}, - {Name: providerAttributeName}, - }, - }, nil) - - if err != nil { - return err - } - - if resources.IsEmpty() { - continue - } - - for _, resource := range resources.Blocks { - providerAlias := "default" - - // Override the provider alias if defined - if val, ok := resource.Body.Attributes[providerAttributeName]; ok { - provider, diagnostics := aws.DecodeProviderConfigRef(val.Expr, "provider") - if diagnostics.HasErrors() { - logger.Error("error decoding provider: %w", diagnostics) - return diagnostics - } - providerAlias = provider.Alias - } - - providerAliasTags := providerTagsMap[providerAlias] - - // If the resource has a tags attribute - if attribute, okResource := resource.Body.Attributes[tagsAttributeName]; okResource { - logger.Debug( - "Walk `%s` attribute", - resource.Labels[0]+"."+resource.Labels[1]+"."+tagsAttributeName, - ) - - err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { - knownTags, known := getKnownForValue(val) - if !known { - logger.Warn("The invalid aws tags rule can only evaluate provided variables, skipping %s.", resource.Labels[0]+"."+resource.Labels[1]+"."+tagsAttributeName) - return nil - } - - // merge the known tags with the provider tags to comply with the implementation - // https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/resource-tagging#propagating-tags-to-all-resources - for providerTagKey, providerTagValue := range providerAliasTags { - if _, ok := knownTags[providerTagKey]; !ok { - knownTags[providerTagKey] = providerTagValue - } - } - - r.emitIssue(runner, knownTags, config, attribute.Expr.Range()) - return nil - }, nil) - - if err != nil { - return err - } - } else { - logger.Debug("Walk `%s` resource", resource.Labels[0]+"."+resource.Labels[1]) - r.emitIssue(runner, providerAliasTags, config, resource.DefRange) - } - } - } - - // Special handling for tags on aws_autoscaling_group resources - if err := r.checkAwsAutoScalingGroups(runner, config); err != nil { - return err - } - - return nil -} - -func (r *AwsResourceInvalidTagsRule) getProviderLevelTags(runner tflint.Runner) (awsProvidersTags, error) { - providerSchema := &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - { - Name: "alias", - Required: false, - }, - }, - Blocks: []hclext.BlockSchema{ - { - Type: defaultTagsBlockName, - Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: tagsAttributeName}}}, - }, - }, - } - - providerBody, err := runner.GetProviderContent("aws", providerSchema, nil) - if err != nil { - return nil, err - } - - // Get provider default tags - allProviderTags := make(awsProvidersTags) - var providerAlias string - - for _, provider := range providerBody.Blocks.OfType(providerAttributeName) { - // Get the alias attribute, in terraform when there is a single aws provider its called "default" - providerAttr, ok := provider.Body.Attributes["alias"] - if !ok { - providerAlias = "default" - } else { - err := runner.EvaluateExpr(providerAttr.Expr, func(alias string) error { - logger.Debug("Walk `%s` provider", providerAlias) - providerAlias = alias - // Init the provider reference even if it doesn't have tags - allProviderTags[alias] = nil - return nil - }, nil) - if err != nil { - return nil, err - } - } - - for _, block := range provider.Body.Blocks { - attr, ok := block.Body.Attributes[tagsAttributeName] - if !ok { - continue - } - - err := runner.EvaluateExpr(attr.Expr, func(val cty.Value) error { - tags, known := getKnownForValue(val) - - if !known { - logger.Warn("The invalid aws tags rule can only evaluate provided variables, skipping %s.", provider.Labels[0]+"."+providerAlias+"."+defaultTagsBlockName+"."+tagsAttributeName) - return nil - } - - logger.Debug("Walk `%s` provider with tags `%v`", providerAlias, tags) - allProviderTags[providerAlias] = tags - return nil - }, nil) - - if err != nil { - return nil, err - } - } - } - return allProviderTags, nil -} - -// checkAwsAutoScalingGroups handles the special case for tags on AutoScaling Groups -// See: https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/autoscaling_tags.go -func (r *AwsResourceInvalidTagsRule) checkAwsAutoScalingGroups(runner tflint.Runner, config awsResourceInvalidTagsRuleConfig) error { - resourceType := "aws_autoscaling_group" - - // Skip autoscaling group check if its type is excluded in configuration - if stringInSlice(resourceType, config.Exclude) { - return nil - } - - resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{}, nil) - if err != nil { - return err - } - - for _, resource := range resources.Blocks { - asgTagBlockTags, tagBlockLocation, err := r.checkAwsAutoScalingGroupsTag(runner, resource) - if err != nil { - return err - } - - asgTagsAttributeTags, tagsAttributeLocation, err := r.checkAwsAutoScalingGroupsTags(runner, resource) - if err != nil { - return err - } - - switch { - case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) > 0: - runner.EmitIssue(r, "Only tag block or tags attribute may be present, but found both", resource.DefRange) - case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) == 0: - r.emitIssue(runner, map[string]string{}, config, resource.DefRange) - case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) == 0: - tags := asgTagBlockTags - location := tagBlockLocation - r.emitIssue(runner, tags, config, location) - case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) > 0: - tags := asgTagsAttributeTags - location := tagsAttributeLocation - r.emitIssue(runner, tags, config, location) - } - } - - return nil -} - -// checkAwsAutoScalingGroupsTag checks tag{} blocks on aws_autoscaling_group resources -func (r *AwsResourceInvalidTagsRule) 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{ - Blocks: []hclext.BlockSchema{ - { - Type: tagBlockName, - Body: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: "key"}, - {Name: "value"}, - }, - }, - }, - }, - }, nil) - if err != nil { - return tags, hcl.Range{}, err - } - - for _, resource := range resources.Blocks { - if resource.Labels[0] != resourceBlock.Labels[0] { - continue - } - - for _, tag := range resource.Body.Blocks { - keyAttribute, keyExists := tag.Body.Attributes["key"] - if !keyExists { - return tags, hcl.Range{}, fmt.Errorf(`Did not find expected field "key" in aws_autoscaling_group "%s" starting at line %d`, resource.Labels[0], resource.DefRange.Start.Line) - } - - valueAttribute, valueExists := tag.Body.Attributes["value"] - if !valueExists { - return tags, hcl.Range{}, fmt.Errorf(`Did not find expected field "value" in aws_autoscaling_group "%s" starting at line %d`, resource.Labels[0], resource.DefRange.Start.Line) - } - - err := runner.EvaluateExpr(keyAttribute.Expr, func(key string) error { - return runner.EvaluateExpr(valueAttribute.Expr, func(value string) error { - tags[key] = value - return nil - }, nil) - }, nil) - if err != nil { - return tags, hcl.Range{}, err - } - } - } - - return tags, resourceBlock.DefRange, nil -} - -// checkAwsAutoScalingGroupsTag checks the tags attribute on aws_autoscaling_group resources -func (r *AwsResourceInvalidTagsRule) 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{ - Attributes: []hclext.AttributeSchema{ - {Name: tagsAttributeName}, - }, - }, nil) - if err != nil { - return tags, hcl.Range{}, err - } - - for _, resource := range resources.Blocks { - if resource.Labels[0] != resourceBlock.Labels[0] { - continue - } - - attribute, ok := resource.Body.Attributes[tagsAttributeName] - if ok { - wantType := cty.List(cty.Object(map[string]cty.Type{ - "key": cty.String, - "value": cty.String, - "propagate_at_launch": cty.Bool, - })) - err := runner.EvaluateExpr(attribute.Expr, func(asgTags []awsAutoscalingGroupTag) error { - for _, tag := range asgTags { - tags[tag.Key] = tag.Value - } - return nil - }, &tflint.EvaluateExprOption{WantType: &wantType}) - if err != nil { - return tags, attribute.Expr.Range(), err - } - return tags, attribute.Expr.Range(), nil - } - } - - return tags, resourceBlock.DefRange, nil -} - -func (r *AwsResourceInvalidTagsRule) emitIssue(runner tflint.Runner, tags map[string]string, config awsResourceInvalidTagsRuleConfig, location hcl.Range) { - // sort the tag names for deterministic output - // only evaluate the given tags on the resource NOT the configured tags - // the checking of tag presence should use the `aws_resource_missing_tags` lint rule - tagsToMatch := sort.StringSlice{} - for tagName := range tags { - tagsToMatch = append(tagsToMatch, tagName) - } - tagsToMatch.Sort() - - str := "" - for _, tagName := range tagsToMatch { - allowedValues, ok := config.Tags[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, ",")) - } - } - } - - if len(str) > 0 { - runner.EmitIssue(r, strings.TrimSpace(str), location) - } -} diff --git a/rules/aws_resource_missing_tags.go b/rules/aws_resource_missing_tags.go deleted file mode 100644 index f5ee513a..00000000 --- a/rules/aws_resource_missing_tags.go +++ /dev/null @@ -1,374 +0,0 @@ -package rules - -import ( - "fmt" - "sort" - "strings" - - hcl "github.com/hashicorp/hcl/v2" - "github.com/terraform-linters/tflint-plugin-sdk/hclext" - "github.com/terraform-linters/tflint-plugin-sdk/logger" - "github.com/terraform-linters/tflint-plugin-sdk/tflint" - "github.com/terraform-linters/tflint-ruleset-aws/aws" - "github.com/terraform-linters/tflint-ruleset-aws/project" - "github.com/terraform-linters/tflint-ruleset-aws/rules/tags" - "github.com/zclconf/go-cty/cty" - "golang.org/x/exp/slices" -) - -// AwsResourceMissingTagsRule checks whether resources are tagged correctly -type AwsResourceMissingTagsRule struct { - tflint.DefaultRule -} - -type awsResourceTagsRuleConfig struct { - Tags []string `hclext:"tags"` - Exclude []string `hclext:"exclude,optional"` -} - -// NewAwsResourceMissingTagsRule returns new rules for all resources that support tags -func NewAwsResourceMissingTagsRule() *AwsResourceMissingTagsRule { - return &AwsResourceMissingTagsRule{} -} - -// Name returns the rule name -func (r *AwsResourceMissingTagsRule) Name() string { - return "aws_resource_missing_tags" -} - -// Enabled returns whether the rule is enabled by default -func (r *AwsResourceMissingTagsRule) Enabled() bool { - return false -} - -// Severity returns the rule severity -func (r *AwsResourceMissingTagsRule) Severity() tflint.Severity { - return tflint.NOTICE -} - -// Link returns the rule reference link -func (r *AwsResourceMissingTagsRule) Link() string { - return project.ReferenceLink(r.Name()) -} - -func (r *AwsResourceMissingTagsRule) getProviderLevelTags(runner tflint.Runner) (map[string][]string, error) { - providerSchema := &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - { - Name: "alias", - Required: false, - }, - }, - Blocks: []hclext.BlockSchema{ - { - Type: defaultTagsBlockName, - Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: tagsAttributeName}}}, - }, - }, - } - - providerBody, err := runner.GetProviderContent("aws", providerSchema, nil) - if err != nil { - return nil, err - } - - // Get provider default tags - allProviderTags := make(map[string][]string) - var providerAlias string - for _, provider := range providerBody.Blocks.OfType(providerAttributeName) { - // Get the alias attribute, in terraform when there is a single aws provider its called "default" - providerAttr, ok := provider.Body.Attributes["alias"] - if !ok { - providerAlias = "default" - } else { - err := runner.EvaluateExpr(providerAttr.Expr, func(alias string) error { - logger.Debug("Walk `%s` provider", providerAlias) - providerAlias = alias - // Init the provider reference even if it doesn't have tags - allProviderTags[alias] = nil - return nil - }, nil) - if err != nil { - return nil, err - } - } - - for _, block := range provider.Body.Blocks { - var providerTags []string - attr, ok := block.Body.Attributes[tagsAttributeName] - if !ok { - continue - } - - err := runner.EvaluateExpr(attr.Expr, func(val cty.Value) error { - keys, known := getKeysForValue(val) - - if !known { - logger.Warn("The missing aws tags rule can only evaluate provided variables, skipping %s.", provider.Labels[0]+"."+providerAlias+"."+defaultTagsBlockName+"."+tagsAttributeName) - return nil - } - - logger.Debug("Walk `%s` provider with tags `%v`", providerAlias, keys) - providerTags = keys - return nil - }, nil) - - if err != nil { - return nil, err - } - - allProviderTags[providerAlias] = providerTags - } - } - return allProviderTags, nil -} - -// Check checks resources for missing tags -func (r *AwsResourceMissingTagsRule) Check(runner tflint.Runner) error { - config := awsResourceTagsRuleConfig{} - if err := runner.DecodeRuleConfig(r.Name(), &config); err != nil { - return err - } - - providerTagsMap, err := r.getProviderLevelTags(runner) - - if err != nil { - return err - } - - for _, resourceType := range tags.Resources { - // Skip this resource if its type is excluded in configuration - if stringInSlice(resourceType, config.Exclude) { - continue - } - - resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: tagsAttributeName}, - {Name: providerAttributeName}, - }, - }, nil) - if err != nil { - return err - } - - if resources.IsEmpty() { - continue - } - - for _, resource := range resources.Blocks { - providerAlias := "default" - // Override the provider alias if defined - if val, ok := resource.Body.Attributes[providerAttributeName]; ok { - provider, diagnostics := aws.DecodeProviderConfigRef(val.Expr, "provider") - if diagnostics.HasErrors() { - logger.Error("error decoding provider: %w", diagnostics) - return diagnostics - } - providerAlias = provider.Alias - } - - // If the resource has a tags attribute - if attribute, okResource := resource.Body.Attributes[tagsAttributeName]; okResource { - logger.Debug( - "Walk `%s` attribute", - resource.Labels[0]+"."+resource.Labels[1]+"."+tagsAttributeName, - ) - - err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { - keys, known := getKeysForValue(val) - if !known { - logger.Warn("The missing aws tags rule can only evaluate provided variables, skipping %s.", resource.Labels[0]+"."+resource.Labels[1]+"."+tagsAttributeName) - return nil - } - - r.emitIssue(runner, append(providerTagsMap[providerAlias], keys...), config, attribute.Expr.Range()) - return nil - }, nil) - - if err != nil { - return err - } - } else { - logger.Debug("Walk `%s` resource", resource.Labels[0]+"."+resource.Labels[1]) - r.emitIssue(runner, providerTagsMap[providerAlias], config, resource.DefRange) - } - } - } - - // Special handling for tags on aws_autoscaling_group resources - if err := r.checkAwsAutoScalingGroups(runner, config); err != nil { - return err - } - - return nil -} - -// checkAwsAutoScalingGroups handles the special case for tags on AutoScaling Groups -// See: https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/autoscaling_tags.go -func (r *AwsResourceMissingTagsRule) checkAwsAutoScalingGroups(runner tflint.Runner, config awsResourceTagsRuleConfig) error { - resourceType := "aws_autoscaling_group" - - // Skip autoscaling group check if its type is excluded in configuration - if stringInSlice(resourceType, config.Exclude) { - return nil - } - - resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{}, nil) - if err != nil { - return err - } - - for _, resource := range resources.Blocks { - asgTagBlockTags, tagBlockLocation, err := r.checkAwsAutoScalingGroupsTag(runner, config, resource) - if err != nil { - return err - } - - asgTagsAttributeTags, tagsAttributeLocation, err := r.checkAwsAutoScalingGroupsTags(runner, config, resource) - if err != nil { - return err - } - - switch { - case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) > 0: - runner.EmitIssue(r, "Only tag block or tags attribute may be present, but found both", resource.DefRange) - case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) == 0: - r.emitIssue(runner, []string{}, config, resource.DefRange) - case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) == 0: - tags := asgTagBlockTags - location := tagBlockLocation - r.emitIssue(runner, tags, config, location) - case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) > 0: - tags := asgTagsAttributeTags - location := tagsAttributeLocation - r.emitIssue(runner, tags, config, location) - } - } - - return nil -} - -// checkAwsAutoScalingGroupsTag checks tag{} blocks on aws_autoscaling_group resources -func (r *AwsResourceMissingTagsRule) checkAwsAutoScalingGroupsTag(runner tflint.Runner, config awsResourceTagsRuleConfig, resourceBlock *hclext.Block) ([]string, hcl.Range, error) { - tags := make([]string, 0) - - resources, err := runner.GetResourceContent("aws_autoscaling_group", &hclext.BodySchema{ - Blocks: []hclext.BlockSchema{ - { - Type: tagBlockName, - Body: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: "key"}, - }, - }, - }, - }, - }, nil) - if err != nil { - return tags, hcl.Range{}, err - } - - for _, resource := range resources.Blocks { - if resource.Labels[0] != resourceBlock.Labels[0] { - continue - } - - for _, tag := range resource.Body.Blocks { - attribute, exists := tag.Body.Attributes["key"] - if !exists { - return tags, hcl.Range{}, fmt.Errorf(`Did not find expected field "key" in aws_autoscaling_group "%s" starting at line %d`, resource.Labels[0], resource.DefRange.Start.Line) - } - - err := runner.EvaluateExpr(attribute.Expr, func(key string) error { - tags = append(tags, key) - return nil - }, nil) - if err != nil { - return tags, hcl.Range{}, err - } - } - } - - return tags, resourceBlock.DefRange, nil -} - -// checkAwsAutoScalingGroupsTag checks the tags attribute on aws_autoscaling_group resources -func (r *AwsResourceMissingTagsRule) checkAwsAutoScalingGroupsTags(runner tflint.Runner, config awsResourceTagsRuleConfig, resourceBlock *hclext.Block) ([]string, hcl.Range, error) { - tags := make([]string, 0) - - resources, err := runner.GetResourceContent("aws_autoscaling_group", &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: tagsAttributeName}, - }, - }, nil) - if err != nil { - return tags, hcl.Range{}, err - } - - for _, resource := range resources.Blocks { - if resource.Labels[0] != resourceBlock.Labels[0] { - continue - } - - attribute, ok := resource.Body.Attributes[tagsAttributeName] - if ok { - wantType := cty.List(cty.Object(map[string]cty.Type{ - "key": cty.String, - "value": cty.String, - "propagate_at_launch": cty.Bool, - })) - err := runner.EvaluateExpr(attribute.Expr, func(asgTags []awsAutoscalingGroupTag) error { - for _, tag := range asgTags { - tags = append(tags, tag.Key) - } - return nil - }, &tflint.EvaluateExprOption{WantType: &wantType}) - if err != nil { - return tags, attribute.Expr.Range(), err - } - return tags, attribute.Expr.Range(), nil - } - } - - return tags, resourceBlock.DefRange, nil -} - -func (r *AwsResourceMissingTagsRule) emitIssue(runner tflint.Runner, tags []string, config awsResourceTagsRuleConfig, location hcl.Range) { - var missing []string - for _, tag := range config.Tags { - if !slices.Contains(tags, tag) { - missing = append(missing, fmt.Sprintf("%q", tag)) - } - } - if len(missing) > 0 { - sort.Strings(missing) - wanted := strings.Join(missing, ", ") - issue := fmt.Sprintf("The resource is missing the following tags: %s.", wanted) - runner.EmitIssue(r, issue, location) - } -} - -// getKeysForValue returns a list of keys from a cty.Value, which is assumed to be a map (or unknown). -// It returns a boolean indicating whether the keys were known. -// If _any_ key is unknown, the entire value is considered unknown, since we can't know if a required tag might be matched by the unknown key. -// Values are entirely ignored and can be unknown. -func getKeysForValue(value cty.Value) (keys []string, known bool) { - if !value.CanIterateElements() || !value.IsKnown() { - return nil, false - } - if value.IsNull() { - return keys, true - } - - return keys, !value.ForEachElement(func(key, _ cty.Value) bool { - // If any key is unknown or sensitive, return early as any missing tag could be this unknown key. - if !key.IsKnown() || key.IsNull() || key.IsMarked() { - return true - } - - keys = append(keys, key.AsString()) - - return false - }) -} diff --git a/rules/aws_resource_missing_tags_test.go b/rules/aws_resource_missing_tags_test.go deleted file mode 100644 index 9725cf32..00000000 --- a/rules/aws_resource_missing_tags_test.go +++ /dev/null @@ -1,547 +0,0 @@ -package rules - -import ( - "testing" - - hcl "github.com/hashicorp/hcl/v2" - "github.com/stretchr/testify/assert" - "github.com/terraform-linters/tflint-plugin-sdk/helper" -) - -func Test_AwsResourceMissingTags(t *testing.T) { - cases := []struct { - Name string - Content string - Config string - Expected helper.Issues - RaiseErr error - }{ - { - Name: "Wanted tags: Bar,Foo, found: bar,foo", - Content: ` -resource "aws_instance" "ec2_instance" { - instance_type = "t2.micro" - tags = { - foo = "bar" - bar = "baz" - } -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] -}`, - Expected: helper.Issues{ - { - Rule: NewAwsResourceMissingTagsRule(), - Message: "The resource is missing the following tags: \"Bar\", \"Foo\".", - Range: hcl.Range{ - Filename: "module.tf", - Start: hcl.Pos{Line: 4, Column: 10}, - End: hcl.Pos{Line: 7, Column: 4}, - }, - }, - }, - }, - { - Name: "No tags", - Content: ` -resource "aws_instance" "ec2_instance" { - instance_type = "t2.micro" -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] -}`, - Expected: helper.Issues{ - { - Rule: NewAwsResourceMissingTagsRule(), - Message: "The resource is missing the following tags: \"Bar\", \"Foo\".", - Range: hcl.Range{ - Filename: "module.tf", - Start: hcl.Pos{Line: 2, Column: 1}, - End: hcl.Pos{Line: 2, Column: 39}, - }, - }, - }, - }, - { - Name: "Tags are correct", - Content: ` -resource "aws_instance" "ec2_instance" { - instance_type = "t2.micro" - tags = { - Foo = "bar" - Bar = "baz" - } -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] -}`, - Expected: helper.Issues{}, - }, - { - Name: "AutoScaling Group with tag blocks and correct tags", - Content: ` -resource "aws_autoscaling_group" "asg" { - tag { - key = "Foo" - value = "bar" - propagate_at_launch = true - } - tag { - key = "Bar" - value = "baz" - propagate_at_launch = true - } -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] -}`, - Expected: helper.Issues{}, - }, - { - Name: "AutoScaling Group with tag blocks and incorrect tags", - Content: ` -resource "aws_autoscaling_group" "asg" { - tag { - key = "Foo" - value = "bar" - propagate_at_launch = true - } -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] -}`, - Expected: helper.Issues{ - { - Rule: NewAwsResourceMissingTagsRule(), - Message: "The resource is missing the following tags: \"Bar\".", - Range: hcl.Range{ - Filename: "module.tf", - Start: hcl.Pos{Line: 2, Column: 1}, - End: hcl.Pos{Line: 2, Column: 39}, - }, - }, - }, - }, - { - Name: "AutoScaling Group with tags attribute and correct tags", - Content: ` -resource "aws_autoscaling_group" "asg" { - tags = [ - { - key = "Foo" - value = "bar" - propagate_at_launch = true - }, - { - key = "Bar" - value = "baz" - propagate_at_launch = true - } - ] -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] -}`, - Expected: helper.Issues{}, - }, - { - Name: "AutoScaling Group with tags attribute and incorrect tags", - Content: ` -resource "aws_autoscaling_group" "asg" { - tags = [ - { - key = "Foo" - value = "bar" - propagate_at_launch = true - } - ] -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] -}`, - Expected: helper.Issues{ - { - Rule: NewAwsResourceMissingTagsRule(), - Message: "The resource is missing the following tags: \"Bar\".", - Range: hcl.Range{ - Filename: "module.tf", - Start: hcl.Pos{Line: 3, Column: 10}, - End: hcl.Pos{Line: 9, Column: 4}, - }, - }, - }, - }, - { - Name: "AutoScaling Group excluded from missing tags rule", - Content: ` -resource "aws_autoscaling_group" "asg" { - tags = [ - { - key = "Foo" - value = "bar" - propagate_at_launch = true - } - ] -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] - exclude = ["aws_autoscaling_group"] -}`, - Expected: helper.Issues{}, - }, - { - Name: "AutoScaling Group with both tag block and tags attribute", - Content: ` -resource "aws_autoscaling_group" "asg" { - tag { - key = "Foo" - value = "bar" - propagate_at_launch = true - } - tags = [ - { - key = "Foo" - value = "bar" - propagate_at_launch = true - } - ] -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] -}`, - Expected: helper.Issues{ - { - Rule: NewAwsResourceMissingTagsRule(), - 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: 1}, - End: hcl.Pos{Line: 2, Column: 39}, - }, - }, - }, - }, - { - Name: "AutoScaling Group with no tags", - Content: ` -resource "aws_autoscaling_group" "asg" { -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] -}`, - Expected: helper.Issues{ - { - Rule: NewAwsResourceMissingTagsRule(), - Message: "The resource is missing the following tags: \"Bar\", \"Foo\".", - Range: hcl.Range{ - Filename: "module.tf", - Start: hcl.Pos{Line: 2, Column: 1}, - End: hcl.Pos{Line: 2, Column: 39}, - }, - }, - }, - }, - { - Name: "Default tags multiple providers", - Content: ` -provider "aws" { - default_tags { - tags = { - "Fooz": "Barz" - "Bazz": "Quxz" - } - } -} - -provider "aws" { - alias = "foo" - default_tags { - tags = { - "Bazz": "Quxz" - "Fooz": "Barz" - } - } -} - -resource "aws_instance" "ec2_instance" { - instance_type = "t2.micro" -} - -resource "aws_instance" "ec2_instance_alias" { - provider = aws.foo - instance_type = "t2.micro" -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Bazz", "Fooz"] -}`, - Expected: helper.Issues{}, - }, - { - Name: "Default Tags Are to Be overriden by resource specific tags", - Content: ` -provider "aws" { - default_tags { - tags = { - "Foo": "Bar" - } - } -} - -resource "aws_instance" "ec2_instance" { - instance_type = "t2.micro" - tags = { - "Foo": "Bazz" - } -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo"] -}`, - Expected: helper.Issues{}, - }, - { - Name: "Resource specific tags are not needed if default tags are placed", - Content: ` -provider "aws" { - default_tags { - tags = { - "Foo": "Bar" - } - } -} - -resource "aws_instance" "ec2_instance" { - instance_type = "t2.micro" -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo"] -}`, - Expected: helper.Issues{}, - }, - { - Name: "Resource tags in combination with provider level tags", - Content: ` -provider "aws" { - default_tags { - tags = { - "Foo": "Bar" - } - } -} - -resource "aws_instance" "ec2_instance_fail" { - instance_type = "t2.micro" -} - - -resource "aws_instance" "ec2_instance" { - instance_type = "t2.micro" - tags = { - "Bazz": "Quazz" - } -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bazz"] -}`, - Expected: helper.Issues{ - { - Rule: NewAwsResourceMissingTagsRule(), - Message: "The resource is missing the following tags: \"Bazz\".", - Range: hcl.Range{ - Filename: "module.tf", - Start: hcl.Pos{Line: 10, Column: 1}, - End: hcl.Pos{Line: 10, Column: 44}, - }, - }, - }, - }, - { - Name: "Provider reference existent without tags definition", - Content: `provider "aws" { - alias = "west" - region = "us-west-2" -} - -resource "aws_ssm_parameter" "param" { - provider = aws.west - name = "test" - type = "String" - value = "test" - tags = { - Foo = "Bar" - } -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo"] -}`, - Expected: helper.Issues{}, - }, - { - Name: "Unknown value maps should be silently ignored", - Content: `variable "aws_region" { - default = "us-east-1" - type = string -} - -provider "aws" { - region = "us-east-1" - alias = "foo" - default_tags { - tags = { - Owner = "Owner" - } - } -} - -resource "aws_s3_bucket" "a" { - provider = aws.foo - name = "a" -} - -resource "aws_s3_bucket" "b" { - name = "b" - tags = var.default_tags -} - -variable "default_tags" { - type = map(string) -} - -provider "aws" { - region = "us-east-1" - alias = "bar" - default_tags { - tags = var.default_tags - } -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Owner"] -}`, - Expected: helper.Issues{}, - }, - { - Name: "Not wholly known tags as value maps should work as long as each key is statically defined", - Content: `variable "owner" { - type = string -} - -variable "project" { - type = string -} - -provider "aws" { - region = "us-east-1" - alias = "foo" - default_tags { - tags = { - Owner = var.owner - Project = var.project - } - } -} - -resource "aws_s3_bucket" "a" { - provider = aws.foo - name = "a" -} - -resource "aws_s3_bucket" "b" { - name = "b" - tags = { - Owner = var.owner - Project = var.project - } -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Owner", "Project"] -}`, - Expected: helper.Issues{}, - }, - { - // In child modules, the provider declarations are passed implicitly or explicitly from the root module. - // In this case, it is not possible to refer to the provider's default_tags. - // Here, the strategy is to ignore default_tags rather than skip inspection of the tags. - Name: "provider aliases within child modules", - Content: ` -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - configuration_aliases = [ aws.foo ] - } - } -} - -resource "aws_instance" "ec2_instance" { - provider = aws.foo -}`, - Config: ` -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] -}`, - Expected: helper.Issues{ - { - Rule: NewAwsResourceMissingTagsRule(), - Message: "The resource is missing the following tags: \"Bar\", \"Foo\".", - Range: hcl.Range{ - Filename: "module.tf", - Start: hcl.Pos{Line: 11, Column: 1}, - End: hcl.Pos{Line: 11, Column: 39}, - }, - }, - }, - }, - } - - rule := NewAwsResourceMissingTagsRule() - - for _, tc := range cases { - t.Run(tc.Name, func(t *testing.T) { - runner := helper.TestRunner(t, map[string]string{"module.tf": tc.Content, ".tflint.hcl": tc.Config}) - - err := rule.Check(runner) - - if tc.RaiseErr == nil && err != nil { - t.Fatalf("Unexpected error occurred in test \"%s\": %s", tc.Name, err) - } - - assert.Equal(t, tc.RaiseErr, err) - - helper.AssertIssues(t, tc.Expected, runner.Issues) - }) - } -} diff --git a/rules/aws_resource_tags.go b/rules/aws_resource_tags.go index 2ebc9048..17df2eb4 100644 --- a/rules/aws_resource_tags.go +++ b/rules/aws_resource_tags.go @@ -1,7 +1,18 @@ package rules import ( + "fmt" + "slices" + "sort" + "strings" + + hcl "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/logger" "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/terraform-linters/tflint-ruleset-aws/aws" + "github.com/terraform-linters/tflint-ruleset-aws/project" + "github.com/terraform-linters/tflint-ruleset-aws/rules/tags" "github.com/zclconf/go-cty/cty" ) @@ -12,10 +23,17 @@ const ( providerAttributeName = "provider" ) +// AwsResourceTagsRule checks whether resources are tagged with valid values type AwsResourceTagsRule struct { tflint.DefaultRule } +type awsResourceTagsRuleConfig struct { + Required []string `hclext:"required,optional"` + Values map[string][]string `hclext:"values,optional"` + Exclude []string `hclext:"exclude,optional"` +} + // awsAutoscalingGroupTag is used by go-cty to evaluate tags in aws_autoscaling_group resources // The type does not need to be public, but its fields do // https://github.com/zclconf/go-cty/blob/master/docs/gocty.md#converting-to-and-from-structs @@ -28,38 +46,355 @@ type awsAutoscalingGroupTag struct { type awsTags map[string]string type awsProvidersTags map[string]awsTags -func stringInSlice(a string, list []string) bool { - for _, b := range list { - if b == a { - return true +// NewAwsResourceTagsRule returns new rules for all resources that support tags +func NewAwsResourceTagsRule() *AwsResourceTagsRule { + return &AwsResourceTagsRule{} +} + +// Name returns the rule name +func (r *AwsResourceTagsRule) Name() string { + return "aws_resource_tags" +} + +// Enabled returns whether the rule is enabled by default +func (r *AwsResourceTagsRule) Enabled() bool { + return false +} + +// Severity returns the rule severity +func (r *AwsResourceTagsRule) Severity() tflint.Severity { + return tflint.NOTICE +} + +// Link returns the rule reference link +func (r *AwsResourceTagsRule) Link() string { + return project.ReferenceLink(r.Name()) +} + +// Check checks resources for invalid tags +func (r *AwsResourceTagsRule) Check(runner tflint.Runner) error { + config := awsResourceTagsRuleConfig{} + if err := runner.DecodeRuleConfig(r.Name(), &config); err != nil { + return err + } + + providerTagsMap, err := r.getProviderLevelTags(runner) + + if err != nil { + return err + } + + for _, resourceType := range tags.Resources { + // Skip this resource if its type is excluded in configuration + if stringInSlice(resourceType, config.Exclude) { + continue + } + + resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: tagsAttributeName}, + {Name: providerAttributeName}, + }, + }, nil) + + if err != nil { + return err + } + + if resources.IsEmpty() { + continue + } + + for _, resource := range resources.Blocks { + providerAlias := "default" + + // Override the provider alias if defined + if val, ok := resource.Body.Attributes[providerAttributeName]; ok { + provider, diagnostics := aws.DecodeProviderConfigRef(val.Expr, "provider") + if diagnostics.HasErrors() { + logger.Error("error decoding provider: %w", diagnostics) + return diagnostics + } + providerAlias = provider.Alias + } + + providerAliasTags := providerTagsMap[providerAlias] + + // If the resource has a tags attribute + if attribute, okResource := resource.Body.Attributes[tagsAttributeName]; okResource { + logger.Debug( + "Walk `%s` attribute", + resource.Labels[0]+"."+resource.Labels[1]+"."+tagsAttributeName, + ) + + err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error { + knownTags, known := getKnownForValue(val) + if !known { + logger.Warn("The invalid aws tags rule can only evaluate provided variables, skipping %s.", resource.Labels[0]+"."+resource.Labels[1]+"."+tagsAttributeName) + return nil + } + + // merge the known tags with the provider tags to comply with the implementation + // https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/resource-tagging#propagating-tags-to-all-resources + for providerTagKey, providerTagValue := range providerAliasTags { + if _, ok := knownTags[providerTagKey]; !ok { + knownTags[providerTagKey] = providerTagValue + } + } + + r.emitIssue(runner, knownTags, config, attribute.Expr.Range()) + return nil + }, nil) + + if err != nil { + return err + } + } else { + logger.Debug("Walk `%s` resource", resource.Labels[0]+"."+resource.Labels[1]) + r.emitIssue(runner, providerAliasTags, config, resource.DefRange) + } } } - return false + + // Special handling for tags on aws_autoscaling_group resources + if err := r.checkAwsAutoScalingGroups(runner, config); err != nil { + return err + } + + return nil +} + +func (r *AwsResourceTagsRule) getProviderLevelTags(runner tflint.Runner) (awsProvidersTags, error) { + providerSchema := &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + { + Name: "alias", + Required: false, + }, + }, + Blocks: []hclext.BlockSchema{ + { + Type: defaultTagsBlockName, + Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: tagsAttributeName}}}, + }, + }, + } + + providerBody, err := runner.GetProviderContent("aws", providerSchema, nil) + if err != nil { + return nil, err + } + + // Get provider default tags + allProviderTags := make(awsProvidersTags) + var providerAlias string + + for _, provider := range providerBody.Blocks.OfType(providerAttributeName) { + // Get the alias attribute, in terraform when there is a single aws provider its called "default" + providerAttr, ok := provider.Body.Attributes["alias"] + if !ok { + providerAlias = "default" + } else { + err := runner.EvaluateExpr(providerAttr.Expr, func(alias string) error { + logger.Debug("Walk `%s` provider", providerAlias) + providerAlias = alias + // Init the provider reference even if it doesn't have tags + allProviderTags[alias] = nil + return nil + }, nil) + if err != nil { + return nil, err + } + } + + for _, block := range provider.Body.Blocks { + attr, ok := block.Body.Attributes[tagsAttributeName] + if !ok { + continue + } + + err := runner.EvaluateExpr(attr.Expr, func(val cty.Value) error { + tags, known := getKnownForValue(val) + + if !known { + logger.Warn("The invalid aws tags rule can only evaluate provided variables, skipping %s.", provider.Labels[0]+"."+providerAlias+"."+defaultTagsBlockName+"."+tagsAttributeName) + return nil + } + + logger.Debug("Walk `%s` provider with tags `%v`", providerAlias, tags) + allProviderTags[providerAlias] = tags + return nil + }, nil) + + if err != nil { + return nil, err + } + } + } + return allProviderTags, nil } -func getKnownForValue(value cty.Value) (map[string]string, bool) { +// checkAwsAutoScalingGroups handles the special case for tags on AutoScaling Groups +// See: https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/autoscaling_tags.go +func (r *AwsResourceTagsRule) checkAwsAutoScalingGroups(runner tflint.Runner, config awsResourceTagsRuleConfig) error { + resourceType := "aws_autoscaling_group" + + // Skip autoscaling group check if its type is excluded in configuration + if stringInSlice(resourceType, config.Exclude) { + return nil + } + + resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{}, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + asgTagBlockTags, tagBlockLocation, err := r.checkAwsAutoScalingGroupsTag(runner, resource) + if err != nil { + return err + } + + asgTagsAttributeTags, tagsAttributeLocation, err := r.checkAwsAutoScalingGroupsTags(runner, resource) + if err != nil { + return err + } + + switch { + case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) > 0: + runner.EmitIssue(r, "Only tag block or tags attribute may be present, but found both", resource.DefRange) + case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) == 0: + r.emitIssue(runner, map[string]string{}, config, resource.DefRange) + case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) == 0: + tags := asgTagBlockTags + location := tagBlockLocation + r.emitIssue(runner, tags, config, location) + case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) > 0: + tags := asgTagsAttributeTags + location := tagsAttributeLocation + r.emitIssue(runner, tags, config, location) + } + } + + return nil +} + +// checkAwsAutoScalingGroupsTag checks tag{} blocks on aws_autoscaling_group resources +func (r *AwsResourceTagsRule) checkAwsAutoScalingGroupsTag(runner tflint.Runner, resourceBlock *hclext.Block) (map[string]string, hcl.Range, error) { tags := map[string]string{} - if !value.CanIterateElements() || !value.IsKnown() { - return nil, false + resources, err := runner.GetResourceContent("aws_autoscaling_group", &hclext.BodySchema{ + Blocks: []hclext.BlockSchema{ + { + Type: tagBlockName, + Body: &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: "key"}, + {Name: "value"}, + }, + }, + }, + }, + }, nil) + if err != nil { + return tags, hcl.Range{}, err } - if value.IsNull() { - return tags, true + + for _, resource := range resources.Blocks { + if resource.Labels[0] != resourceBlock.Labels[0] { + continue + } + + for _, tag := range resource.Body.Blocks { + keyAttribute, keyExists := tag.Body.Attributes["key"] + if !keyExists { + return tags, hcl.Range{}, fmt.Errorf(`Did not find expected field "key" in aws_autoscaling_group "%s" starting at line %d`, resource.Labels[0], resource.DefRange.Start.Line) + } + + valueAttribute, valueExists := tag.Body.Attributes["value"] + if !valueExists { + return tags, hcl.Range{}, fmt.Errorf(`Did not find expected field "value" in aws_autoscaling_group "%s" starting at line %d`, resource.Labels[0], resource.DefRange.Start.Line) + } + + err := runner.EvaluateExpr(keyAttribute.Expr, func(key string) error { + return runner.EvaluateExpr(valueAttribute.Expr, func(value string) error { + tags[key] = value + return nil + }, nil) + }, nil) + if err != nil { + return tags, hcl.Range{}, err + } + } } - return tags, !value.ForEachElement(func(key, value cty.Value) bool { - // If any key is unknown or sensitive, return early as any missing tag could be this unknown key. - if !key.IsKnown() || key.IsNull() || key.IsMarked() { - return true + return tags, resourceBlock.DefRange, nil +} + +// checkAwsAutoScalingGroupsTag checks the tags attribute on aws_autoscaling_group resources +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{ + Attributes: []hclext.AttributeSchema{ + {Name: tagsAttributeName}, + }, + }, nil) + if err != nil { + return tags, hcl.Range{}, err + } + + for _, resource := range resources.Blocks { + if resource.Labels[0] != resourceBlock.Labels[0] { + continue } - if !value.IsKnown() || value.IsMarked() { - return true + attribute, ok := resource.Body.Attributes[tagsAttributeName] + if ok { + wantType := cty.List(cty.Object(map[string]cty.Type{ + "key": cty.String, + "value": cty.String, + "propagate_at_launch": cty.Bool, + })) + err := runner.EvaluateExpr(attribute.Expr, func(asgTags []awsAutoscalingGroupTag) error { + for _, tag := range asgTags { + tags[tag.Key] = tag.Value + } + return nil + }, &tflint.EvaluateExprOption{WantType: &wantType}) + if err != nil { + return tags, attribute.Expr.Range(), err + } + return tags, attribute.Expr.Range(), nil } + } + + return tags, resourceBlock.DefRange, nil +} - // We assume the value of the tag is ALWAYS a string - tags[key.AsString()] = value.AsString() +func (r *AwsResourceTagsRule) emitIssue(runner tflint.Runner, tags map[string]string, config awsResourceTagsRuleConfig, location hcl.Range) { + // sort the tag names for deterministic output + // only evaluate the given tags on the resource NOT the configured tags + // the checking of tag presence should use the `aws_resource_missing_tags` lint rule + tagsToMatch := sort.StringSlice{} + for tagName := range tags { + tagsToMatch = append(tagsToMatch, tagName) + } + tagsToMatch.Sort() - return false - }) + str := "" + 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, ",")) + } + } + } + + if len(str) > 0 { + runner.EmitIssue(r, strings.TrimSpace(str), location) + } } diff --git a/rules/aws_resource_invalid_tags_test.go b/rules/aws_resource_tags_test.go similarity index 76% rename from rules/aws_resource_invalid_tags_test.go rename to rules/aws_resource_tags_test.go index f763751b..19e79dcf 100644 --- a/rules/aws_resource_invalid_tags_test.go +++ b/rules/aws_resource_tags_test.go @@ -8,10 +8,11 @@ import ( "github.com/terraform-linters/tflint-plugin-sdk/helper" ) -const testInvalidTagRule = ` -rule "aws_resource_invalid_tags" { +// TODO: Test Required attr +const testTagRule = ` +rule "aws_resource_tags" { enabled = true - tags = { A: ["1", "foo"], B: ["2", "bar"] } + values = { A: ["1", "foo"], B: ["2", "bar"] } } ` func Test_AwsResourceInvalidTags(t *testing.T) { @@ -28,7 +29,7 @@ func Test_AwsResourceInvalidTags(t *testing.T) { provider "aws" { region = "us-east-1" } resource "aws_instance" "ec2_instance" { } resource "aws_instance" "ec2_instance" { }`, - Config: testInvalidTagRule, + Config: testTagRule, Expected: helper.Issues{}, }, { @@ -38,14 +39,14 @@ func Test_AwsResourceInvalidTags(t *testing.T) { resource "aws_instance" "ec2_instance_two" { tags = { A: "xar", B: "zar" } } resource "aws_s3_bucket" "s3_bucket_one" { tags = { A: "xar", B: "zar" } }`, Config: ` - rule "aws_resource_invalid_tags" { + rule "aws_resource_tags" { enabled = true - tags = { A: ["1", "foo"], B: ["2", "bar"] } + values = { A: ["1", "foo"], B: ["2", "bar"] } exclude = ["aws_instance"] }`, Expected: helper.Issues{ { - Rule: NewAwsResourceInvalidTagsRule(), + Rule: NewAwsResourceTagsRule(), 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", @@ -63,9 +64,48 @@ func Test_AwsResourceInvalidTags(t *testing.T) { } resource "aws_instance" "ec2_instance_one" {} resource "aws_instance" "ec2_instance_two" {}`, - Config: testInvalidTagRule, + Config: testTagRule, Expected: helper.Issues{}, }, + { + Name: "valid provider tags assigned with variables with no explicit tags assigned to resources", + Content: ` + variable "tags" { + type = map(string, string) + default = { A = "1", B = "2" } + } + provider "aws" { + default_tags { tags = var.tags } + } + resource "aws_instance" "ec2_instance_one" {} + resource "aws_instance" "ec2_instance_two" {}`, + Config: testTagRule, + Expected: helper.Issues{}, + }, + { + Name: "invalid provider tags assigned with variables with no explicit tags assigned to resources", + Content: ` + variable "tags" { + type = map(string, string) + default = { A = "1", B = "zar" } + } + provider "aws" { + default_tags { tags = var.tags } + } + resource "aws_instance" "ec2_instance_one" {}`, + Config: testTagRule, + Expected: helper.Issues{ + { + Rule: NewAwsResourceTagsRule(), + Message: "Received 'zar' for tag 'B', expected one of '2,bar'.", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 9, Column: 4}, + End: hcl.Pos{Line: 9, Column: 46}, + }, + }, + }, + }, { Name: "valid provider tags assigned and invalid explicit tags assigned to resources", Content: ` @@ -73,10 +113,10 @@ func Test_AwsResourceInvalidTags(t *testing.T) { default_tags { tags = { A = "1", B = "2" } } } resource "aws_instance" "ec2_instance_one" { tags = { A = "0" }}`, - Config: testInvalidTagRule, + Config: testTagRule, Expected: helper.Issues{ { - Rule: NewAwsResourceInvalidTagsRule(), + Rule: NewAwsResourceTagsRule(), Message: "Received '0' for tag 'A', expected one of '1,foo'.", Range: hcl.Range{ Filename: "module.tf", @@ -93,7 +133,7 @@ func Test_AwsResourceInvalidTags(t *testing.T) { default_tags { tags = { A = "1", B = "2" } } } resource "aws_autoscaling_group" "asg" {}`, - Config: testInvalidTagRule, + Config: testTagRule, Expected: helper.Issues{}, }, // NOTE: This test surfaces the unknown relationship between Provider default tags @@ -105,7 +145,7 @@ func Test_AwsResourceInvalidTags(t *testing.T) { default_tags { tags = { A = "0", B = "0" } } } resource "aws_autoscaling_group" "asg" {}`, - Config: testInvalidTagRule, + Config: testTagRule, Expected: helper.Issues{}, }, { @@ -115,10 +155,10 @@ func Test_AwsResourceInvalidTags(t *testing.T) { default_tags { tags = { A = "0", B = "foo" } } } resource "aws_s3_bucket" "bucket" {}`, - Config: testInvalidTagRule, + Config: testTagRule, Expected: helper.Issues{ { - Rule: NewAwsResourceInvalidTagsRule(), + Rule: NewAwsResourceTagsRule(), 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", @@ -141,7 +181,7 @@ func Test_AwsResourceInvalidTags(t *testing.T) { } resource "aws_instance" "ec2_instance" { provider = "one" } resource "aws_instance" "ec2_instance" { provider = "two" }`, - Config: testInvalidTagRule, + Config: testTagRule, Expected: helper.Issues{}, }, { @@ -149,10 +189,10 @@ func Test_AwsResourceInvalidTags(t *testing.T) { Content: ` resource "aws_instance" "ec2_instance" { tags = { A = "0" } } resource "aws_instance" "ec2_instance" { tags = { B = "0" } }`, - Config: testInvalidTagRule, + Config: testTagRule, Expected: helper.Issues{ { - Rule: NewAwsResourceInvalidTagsRule(), + Rule: NewAwsResourceTagsRule(), Message: "Received '0' for tag 'A', expected one of '1,foo'.", Range: hcl.Range{ Filename: "module.tf", @@ -161,7 +201,7 @@ func Test_AwsResourceInvalidTags(t *testing.T) { }, }, { - Rule: NewAwsResourceInvalidTagsRule(), + Rule: NewAwsResourceTagsRule(), Message: "Received '0' for tag 'B', expected one of '2,bar'.", Range: hcl.Range{ Filename: "module.tf", @@ -174,13 +214,13 @@ func Test_AwsResourceInvalidTags(t *testing.T) { { Name: "explicit resource tag assignment with valid values", Content: `resource "aws_instance" "ec2_instance" { tags = { A = "1", B = "bar" } }`, - Config: testInvalidTagRule, + 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: testInvalidTagRule, + Config: testTagRule, Expected: helper.Issues{}, }, { @@ -196,10 +236,10 @@ func Test_AwsResourceInvalidTags(t *testing.T) { value = "foo" } }`, - Config: testInvalidTagRule, + Config: testTagRule, Expected: helper.Issues{ { - Rule: NewAwsResourceInvalidTagsRule(), + Rule: NewAwsResourceTagsRule(), 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", @@ -222,12 +262,12 @@ func Test_AwsResourceInvalidTags(t *testing.T) { value = "bar" } }`, - Config: testInvalidTagRule, + Config: testTagRule, Expected: helper.Issues{}, }, } - rule := NewAwsResourceInvalidTagsRule() + rule := NewAwsResourceTagsRule() for _, tc := range cases { t.Run(tc.Name, func(t *testing.T) { diff --git a/rules/provider.go b/rules/provider.go index 9570d2a7..e3a1b5af 100644 --- a/rules/provider.go +++ b/rules/provider.go @@ -21,7 +21,6 @@ var manualRules = []tflint.Rule{ NewAwsInstancePreviousTypeRule(), NewAwsMqBrokerInvalidEngineTypeRule(), NewAwsMqConfigurationInvalidEngineTypeRule(), - NewAwsResourceMissingTagsRule(), NewAwsRouteNotSpecifiedTargetRule(), NewAwsRouteSpecifiedMultipleTargetsRule(), NewAwsS3BucketInvalidACLRule(), @@ -40,7 +39,7 @@ var manualRules = []tflint.Rule{ NewAwsSecurityGroupInvalidProtocolRule(), NewAwsSecurityGroupRuleInvalidProtocolRule(), NewAwsProviderMissingDefaultTagsRule(), - NewAwsResourceInvalidTagsRule(), + NewAwsResourceTagsRule(), } // Rules is a list of all rules diff --git a/rules/utils.go b/rules/utils.go index 5278af4b..df97f3ad 100644 --- a/rules/utils.go +++ b/rules/utils.go @@ -1,5 +1,7 @@ package rules +import "github.com/zclconf/go-cty/cty" + var validElastiCacheNodeTypes = map[string]bool{ // https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html "cache.t2.micro": true, @@ -99,3 +101,60 @@ var previousElastiCacheNodeTypes = map[string]bool{ "r3": true, "t1": true, } + +// getKeysForValue returns a list of keys from a cty.Value, which is assumed to be a map (or unknown). +// It returns a boolean indicating whether the keys were known. +// If _any_ key is unknown, the entire value is considered unknown, since we can't know if a required tag might be matched by the unknown key. +// Values are entirely ignored and can be unknown. +func getKeysForValue(value cty.Value) (keys []string, known bool) { + if !value.CanIterateElements() || !value.IsKnown() { + return nil, false + } + if value.IsNull() { + return keys, true + } + return keys, !value.ForEachElement(func(key, _ cty.Value) bool { + // If any key is unknown or sensitive, return early as any missing tag could be this unknown key. + if !key.IsKnown() || key.IsNull() || key.IsMarked() { + return true + } + keys = append(keys, key.AsString()) + return false + }) +} + +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +func getKnownForValue(value cty.Value) (map[string]string, bool) { + tags := map[string]string{} + + if !value.CanIterateElements() || !value.IsKnown() { + return nil, false + } + if value.IsNull() { + return tags, true + } + + return tags, !value.ForEachElement(func(key, value cty.Value) bool { + // If any key is unknown or sensitive, return early as any missing tag could be this unknown key. + if !key.IsKnown() || key.IsNull() || key.IsMarked() { + return true + } + + if !value.IsKnown() || value.IsMarked() { + return true + } + + // We assume the value of the tag is ALWAYS a string + tags[key.AsString()] = value.AsString() + + return false + }) +} From fd7235afb3eab6d07ff3fc0ba64c7c10a86821b6 Mon Sep 17 00:00:00 2001 From: Marc Watts Date: Tue, 11 Jun 2024 13:40:16 +0100 Subject: [PATCH 4/9] 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() From d169516e186683c76082731a8898004f58e7a38c Mon Sep 17 00:00:00 2001 From: Marc Watts Date: Tue, 11 Jun 2024 13:46:32 +0100 Subject: [PATCH 5/9] Amend docs --- docs/rules/aws_resource_missing_tags.md | 76 ------------------- ...e_invalid_tags.md => aws_resource_tags.md} | 15 ++-- 2 files changed, 8 insertions(+), 83 deletions(-) delete mode 100644 docs/rules/aws_resource_missing_tags.md rename docs/rules/{aws_resource_invalid_tags.md => aws_resource_tags.md} (71%) diff --git a/docs/rules/aws_resource_missing_tags.md b/docs/rules/aws_resource_missing_tags.md deleted file mode 100644 index f8f31e40..00000000 --- a/docs/rules/aws_resource_missing_tags.md +++ /dev/null @@ -1,76 +0,0 @@ -# aws_resource_missing_tags - -Require specific tags for all AWS resource types that support them. - -## Configuration - -```hcl -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Foo", "Bar"] - exclude = ["aws_autoscaling_group"] # (Optional) Exclude some resource types from tag checks -} -``` - -## Examples - -Most resources use the `tags` attribute with simple `key`=`value` pairs: - -```hcl -resource "aws_instance" "instance" { - instance_type = "m5.large" - tags = { - foo = "Bar" - bar = "Baz" - } -} -``` - -``` -$ tflint -1 issue(s) found: - -Notice: aws_instance.instance is missing the following tags: "Bar", "Foo". (aws_resource_missing_tags) - - on test.tf line 3: - 3: tags = { - 4: foo = "Bar" - 5: bar = "Baz" - 6: } -``` - -Iterators in `dynamic` blocks cannot be expanded, so the tags in the following example will not be detected. - -```hcl -locals { - tags = [ - { - key = "Name", - value = "SomeName", - }, - { - key = "env", - value = "SomeEnv", - }, - ] -} -resource "aws_autoscaling_group" "this" { - dynamic "tag" { - for_each = local.tags - - content { - key = tag.key - value = tag.value - propagate_at_launch = true - } - } -} -``` - -## Why - -You want to set a standardized set of tags for your AWS resources. - -## How To Fix - -For each resource type that supports tags, ensure that each missing tag is present. diff --git a/docs/rules/aws_resource_invalid_tags.md b/docs/rules/aws_resource_tags.md similarity index 71% rename from docs/rules/aws_resource_invalid_tags.md rename to docs/rules/aws_resource_tags.md index 015a922a..bd203772 100644 --- a/docs/rules/aws_resource_invalid_tags.md +++ b/docs/rules/aws_resource_tags.md @@ -1,17 +1,18 @@ -# aws_resource_invalid_tags +# aws_resource_tags -Require tags to be assigned to a specific set of values. +Rule for resources tag presence and value validation from prefixed list. ## Example ```hcl -rule "aws_resource_invalid_tags" { - enabled = true - tags = { +rule "aws_resource_tags" { + enabled = true + exclude = ["aws_autoscaling_group"] + required = ["Environment"] + values = { Department = ["finance", "hr", "payments", "engineering"] Environment = ["sandbox", "staging", "production"] } - exclude = ["aws_autoscaling_group"] } provider "aws" { @@ -23,7 +24,7 @@ provider "aws" { resource "aws_s3_bucket" "bucket" { ... - tags = { Project: "homepage", Department = "science" } + tags = { Project: "homepage", Department: "science" } } ``` From beff0bfef5c71c73c5f3710da7d0c389b15c26b4 Mon Sep 17 00:00:00 2001 From: Marc Watts Date: Tue, 11 Jun 2024 13:50:29 +0100 Subject: [PATCH 6/9] Amend integration map-attribute --- integration/map-attribute/.tflint.hcl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/map-attribute/.tflint.hcl b/integration/map-attribute/.tflint.hcl index d38db6fb..6233a377 100644 --- a/integration/map-attribute/.tflint.hcl +++ b/integration/map-attribute/.tflint.hcl @@ -6,7 +6,7 @@ plugin "aws" { enabled = true } -rule "aws_resource_missing_tags" { +rule "aws_resource_tags" { enabled = true tags = ["Environment", "Name", "Type"] } From 80784059292c48080d1380c7dc10f2fac638f7ad Mon Sep 17 00:00:00 2001 From: Marc Watts Date: Tue, 11 Jun 2024 14:08:56 +0100 Subject: [PATCH 7/9] Amend references to previous tag rule name --- docs/rules/README.md | 2 +- docs/rules/README.md.tmpl | 2 +- docs/rules/aws_provider_missing_default_tags.md | 11 ++++------- integration/cty-based-eval/.tflint.hcl | 6 +++--- integration/cty-based-eval/result.json | 4 ++-- integration/map-attribute/.tflint.hcl | 4 ++-- integration/map-attribute/result.json | 4 ++-- rules/aws_resource_tags.go | 2 -- 8 files changed, 15 insertions(+), 20 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index e3e0d7fb..bb159d68 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -72,7 +72,7 @@ These rules enforce best practices and naming conventions: |[aws_iam_policy_gov_friendly_arns](aws_iam_policy_gov_friendly_arns.md)|Ensure `iam_policy` resources do not contain `arn:aws:` ARN's|| |[aws_iam_role_policy_gov_friendly_arns](aws_iam_role_policy_gov_friendly_arns.md)|Ensure `iam_role_policy` resources do not contain `arn:aws:` ARN's|| |[aws_lambda_function_deprecated_runtime](aws_lambda_function_deprecated_runtime.md)|Disallow deprecated runtimes for Lambda Function|✔| -|[aws_resource_missing_tags](aws_resource_missing_tags.md)|Require specific tags for all AWS resource types that support them|| +|[aws_resource_tags](aws_resource_tags.md)|Require specific tags for all AWS resource types that support them|| |[aws_s3_bucket_name](aws_s3_bucket_name.md)|Ensures all S3 bucket names match the naming rules|✔| |[aws_provider_missing_default_tags](aws_provider_missing_default_tags.md)|Require specific tags for all AWS providers default tags|| diff --git a/docs/rules/README.md.tmpl b/docs/rules/README.md.tmpl index f197f6fe..aa9b097b 100644 --- a/docs/rules/README.md.tmpl +++ b/docs/rules/README.md.tmpl @@ -72,7 +72,7 @@ These rules enforce best practices and naming conventions: |[aws_iam_policy_gov_friendly_arns](aws_iam_policy_gov_friendly_arns.md)|Ensure `iam_policy` resources do not contain `arn:aws:` ARN's|| |[aws_iam_role_policy_gov_friendly_arns](aws_iam_role_policy_gov_friendly_arns.md)|Ensure `iam_role_policy` resources do not contain `arn:aws:` ARN's|| |[aws_lambda_function_deprecated_runtime](aws_lambda_function_deprecated_runtime.md)|Disallow deprecated runtimes for Lambda Function|✔| -|[aws_resource_missing_tags](aws_resource_missing_tags.md)|Require specific tags for all AWS resource types that support them|| +|[aws_resource_tags](aws_resource_tags.md)|Require specific tags for all AWS resource types that support them|| |[aws_s3_bucket_name](aws_s3_bucket_name.md)|Ensures all S3 bucket names match the naming rules|✔| |[aws_provider_missing_default_tags](aws_provider_missing_default_tags.md)|Require specific tags for all AWS providers default tags|| diff --git a/docs/rules/aws_provider_missing_default_tags.md b/docs/rules/aws_provider_missing_default_tags.md index 613225bd..f4feb3fe 100644 --- a/docs/rules/aws_provider_missing_default_tags.md +++ b/docs/rules/aws_provider_missing_default_tags.md @@ -45,18 +45,15 @@ Notice: The provider is missing the following tags: "Bar", "Foo". (aws_provider_ - Using default tags results in better tagging coverage. The resource missing tags rule needs support to be added for non-standard uses of tags in the provider, for example EC2 root block devices. -Use this rule in conjuction with aws_resource_missing_tags_rule, for example to enforce common tags and +Use this rule in conjuction with aws_resource_tags_rule, for example to enforce common tags and resource specific tags, without duplicating tags. ```hcl -rule "aws_resource_missing_tags" { - enabled = true - tags = [ +rule "aws_resource_tags" { + enabled = true + required = [ "kubernetes.io/cluster/eks", ] - include = [ - "aws_subnet", - ] } rule "aws_provider_missing_default_tags" { diff --git a/integration/cty-based-eval/.tflint.hcl b/integration/cty-based-eval/.tflint.hcl index d38db6fb..d8c8e227 100644 --- a/integration/cty-based-eval/.tflint.hcl +++ b/integration/cty-based-eval/.tflint.hcl @@ -6,7 +6,7 @@ plugin "aws" { enabled = true } -rule "aws_resource_missing_tags" { - enabled = true - tags = ["Environment", "Name", "Type"] +rule "aws_resource_tags" { + enabled = true + required = ["Environment", "Name", "Type"] } diff --git a/integration/cty-based-eval/result.json b/integration/cty-based-eval/result.json index 96de2ef6..2713b08d 100644 --- a/integration/cty-based-eval/result.json +++ b/integration/cty-based-eval/result.json @@ -2,9 +2,9 @@ "issues": [ { "rule": { - "name": "aws_resource_missing_tags", + "name": "aws_resource_tags", "severity": "info", - "link": "https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.32.0/docs/rules/aws_resource_missing_tags.md" + "link": "https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.32.0/docs/rules/aws_resource_tags.md" }, "message": "The resource is missing the following tags: \"Environment\", \"Name\", \"Type\".", "range": { diff --git a/integration/map-attribute/.tflint.hcl b/integration/map-attribute/.tflint.hcl index 6233a377..d8c8e227 100644 --- a/integration/map-attribute/.tflint.hcl +++ b/integration/map-attribute/.tflint.hcl @@ -7,6 +7,6 @@ plugin "aws" { } rule "aws_resource_tags" { - enabled = true - tags = ["Environment", "Name", "Type"] + enabled = true + required = ["Environment", "Name", "Type"] } diff --git a/integration/map-attribute/result.json b/integration/map-attribute/result.json index 912f0cee..6a0da4d2 100644 --- a/integration/map-attribute/result.json +++ b/integration/map-attribute/result.json @@ -2,9 +2,9 @@ "issues": [ { "rule": { - "name": "aws_resource_missing_tags", + "name": "aws_resource_tags", "severity": "info", - "link": "https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.32.0/docs/rules/aws_resource_missing_tags.md" + "link": "https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.32.0/docs/rules/aws_resource_tags.md" }, "message": "The resource is missing the following tags: \"Environment\", \"Name\", \"Type\".", "range": { diff --git a/rules/aws_resource_tags.go b/rules/aws_resource_tags.go index f4e73605..01563f4b 100644 --- a/rules/aws_resource_tags.go +++ b/rules/aws_resource_tags.go @@ -376,8 +376,6 @@ func (r *AwsResourceTagsRule) checkAwsAutoScalingGroupsTags(runner tflint.Runner func (r *AwsResourceTagsRule) emitIssue(runner tflint.Runner, tags map[string]string, config awsResourceTagsRuleConfig, location hcl.Range) { // sort the tag names for deterministic output - // only evaluate the given tags on the resource NOT the configured tags - // the checking of tag presence should use the `aws_resource_missing_tags` lint rule tagsToMatch := sort.StringSlice{} for tagName := range tags { tagsToMatch = append(tagsToMatch, tagName) From 096536ffae891a1a92051e8f74339b48804e64a1 Mon Sep 17 00:00:00 2001 From: Marc Watts Date: Tue, 11 Jun 2024 14:12:33 +0100 Subject: [PATCH 8/9] Amend integration test --- integration/cty-based-eval/result.json | 2 +- integration/map-attribute/result.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/cty-based-eval/result.json b/integration/cty-based-eval/result.json index 2713b08d..f971b147 100644 --- a/integration/cty-based-eval/result.json +++ b/integration/cty-based-eval/result.json @@ -6,7 +6,7 @@ "severity": "info", "link": "https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.32.0/docs/rules/aws_resource_tags.md" }, - "message": "The resource is missing the following tags: \"Environment\", \"Name\", \"Type\".", + "message": "Tag 'Environment' is required. Tag 'Name' is required. Tag 'Type' is required.", "range": { "filename": "template.tf", "start": { diff --git a/integration/map-attribute/result.json b/integration/map-attribute/result.json index 6a0da4d2..ebddce0e 100644 --- a/integration/map-attribute/result.json +++ b/integration/map-attribute/result.json @@ -6,7 +6,7 @@ "severity": "info", "link": "https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.32.0/docs/rules/aws_resource_tags.md" }, - "message": "The resource is missing the following tags: \"Environment\", \"Name\", \"Type\".", + "message": "Tag 'Environment' is required. Tag 'Name' is required. Tag 'Type' is required.", "range": { "filename": "template.tf", "start": { From 994f4d1538c60126f4898ae5954bc889601a3c89 Mon Sep 17 00:00:00 2001 From: Marc Watts Date: Tue, 11 Jun 2024 21:35:49 +0100 Subject: [PATCH 9/9] [whitespace] Amend test --- rules/aws_resource_tags_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rules/aws_resource_tags_test.go b/rules/aws_resource_tags_test.go index 5680c031..c8c96fd7 100644 --- a/rules/aws_resource_tags_test.go +++ b/rules/aws_resource_tags_test.go @@ -10,9 +10,9 @@ import ( const testTagRule = ` rule "aws_resource_tags" { - enabled = true + enabled = true required = ["A", "B"] - values = { A: ["1", "foo"], B: ["2", "bar"] } + values = { A: ["1", "foo"], B: ["2", "bar"] } } ` func Test_AwsResourceTags(t *testing.T) {