Skip to content

Commit 51d759d

Browse files
New rule: aws_provider_missing_tags (#633)
* provider missing tags rule * revert version change * switch to standard slices package * rename aws_provider_missing_tags to aws_provider_missing_default_tags * more descriptive default provider messages, return error from emitIssue * use attr.Range instead of provider.DefRange * update docs * hcl code block * enabled * rename rule in docs
1 parent 120b03a commit 51d759d

6 files changed

+402
-0
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ These rules enforce best practices and naming conventions:
7474
|[aws_lambda_function_deprecated_runtime](aws_lambda_function_deprecated_runtime.md)|Disallow deprecated runtimes for Lambda Function||
7575
|[aws_resource_missing_tags](aws_resource_missing_tags.md)|Require specific tags for all AWS resource types that support them||
7676
|[aws_s3_bucket_name](aws_s3_bucket_name.md)|Ensures all S3 bucket names match the naming rules||
77+
|[aws_provider_missing_default_tags](aws_provider_missing_default_tags.md)|Require specific tags for all AWS providers default tags||
7778

7879
### SDK-based Validations
7980

docs/rules/README.md.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ These rules enforce best practices and naming conventions:
7474
|[aws_lambda_function_deprecated_runtime](aws_lambda_function_deprecated_runtime.md)|Disallow deprecated runtimes for Lambda Function|✔|
7575
|[aws_resource_missing_tags](aws_resource_missing_tags.md)|Require specific tags for all AWS resource types that support them||
7676
|[aws_s3_bucket_name](aws_s3_bucket_name.md)|Ensures all S3 bucket names match the naming rules|✔|
77+
|[aws_provider_missing_default_tags](aws_provider_missing_default_tags.md)|Require specific tags for all AWS providers default tags||
7778

7879
### SDK-based Validations
7980

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# aws_provider_missing_default_tags
2+
3+
Require specific tags for all AWS provider default_tags blocks.
4+
5+
## Configuration
6+
7+
```hcl
8+
rule "aws_provider_missing_default_tags" {
9+
enabled = true
10+
tags = ["Foo", "Bar"]
11+
}
12+
```
13+
14+
## Examples
15+
16+
The `tags` attribute is a map of `key`=`value` pairs, like resource tags:
17+
18+
```hcl
19+
provider "aws" {
20+
default_tags {
21+
tags = {
22+
foo = "Bar"
23+
bar = "Baz"
24+
}
25+
}
26+
}
27+
```
28+
29+
An issue is reported because the tag keys ["foo", "bar"] don't match the required tags ["Foo", "Bar"]
30+
31+
```
32+
$ tflint
33+
1 issue(s) found:
34+
35+
Notice: The provider is missing the following tags: "Bar", "Foo". (aws_provider_missing_default_tags)
36+
37+
on test.tf line 1:
38+
1: provider "aws" {
39+
```
40+
41+
## Why
42+
43+
- You want to set a standardized set of tags for your AWS resources via the provider default tags.
44+
- You want more DRY (don't repeat yourself) Terraform code, by eliminating tag configuration from resources.
45+
- Using default tags results in better tagging coverage. The resource missing tags rule needs support
46+
to be added for non-standard uses of tags in the provider, for example EC2 root block devices.
47+
48+
Use this rule in conjuction with aws_resource_missing_tags_rule, for example to enforce common tags and
49+
resource specific tags, without duplicating tags.
50+
51+
```hcl
52+
rule "aws_resource_missing_tags" {
53+
enabled = true
54+
tags = [
55+
"kubernetes.io/cluster/eks",
56+
]
57+
include = [
58+
"aws_subnet",
59+
]
60+
}
61+
62+
rule "aws_provider_missing_default_tags" {
63+
enabled = true
64+
tags = [
65+
"CostCenter",
66+
]
67+
}
68+
```
69+
70+
## How To Fix
71+
72+
Ensure the provider default tags contains all your required tags.
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package rules
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
"sort"
7+
"strings"
8+
9+
hcl "github.com/hashicorp/hcl/v2"
10+
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
11+
"github.com/terraform-linters/tflint-plugin-sdk/logger"
12+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
13+
"github.com/terraform-linters/tflint-ruleset-aws/project"
14+
"github.com/zclconf/go-cty/cty"
15+
)
16+
17+
// AwsProviderMissingDefaultTagsRule checks whether providers are tagged correctly
18+
type AwsProviderMissingDefaultTagsRule struct {
19+
tflint.DefaultRule
20+
}
21+
22+
type awsProviderTagsRuleConfig struct {
23+
Tags []string `hclext:"tags"`
24+
}
25+
26+
const (
27+
providerDefaultTagsBlockName = "default_tags"
28+
providerTagsAttributeName = "tags"
29+
)
30+
31+
// NewAwsProviderMissingDefaultTagsRule returns new rules for all providers that support tags
32+
func NewAwsProviderMissingDefaultTagsRule() *AwsProviderMissingDefaultTagsRule {
33+
return &AwsProviderMissingDefaultTagsRule{}
34+
}
35+
36+
// Name returns the rule name
37+
func (r *AwsProviderMissingDefaultTagsRule) Name() string {
38+
return "aws_provider_missing_default_tags"
39+
}
40+
41+
// Enabled returns whether the rule is enabled by default
42+
func (r *AwsProviderMissingDefaultTagsRule) Enabled() bool {
43+
return false
44+
}
45+
46+
// Severity returns the rule severity
47+
func (r *AwsProviderMissingDefaultTagsRule) Severity() tflint.Severity {
48+
return tflint.NOTICE
49+
}
50+
51+
// Link returns the rule reference link
52+
func (r *AwsProviderMissingDefaultTagsRule) Link() string {
53+
return project.ReferenceLink(r.Name())
54+
}
55+
56+
// Check checks providers for missing tags
57+
func (r *AwsProviderMissingDefaultTagsRule) Check(runner tflint.Runner) error {
58+
config := awsProviderTagsRuleConfig{}
59+
if err := runner.DecodeRuleConfig(r.Name(), &config); err != nil {
60+
return err
61+
}
62+
63+
providerSchema := &hclext.BodySchema{
64+
Attributes: []hclext.AttributeSchema{
65+
{
66+
Name: "alias",
67+
Required: false,
68+
},
69+
},
70+
Blocks: []hclext.BlockSchema{
71+
{
72+
Type: providerDefaultTagsBlockName,
73+
Body: &hclext.BodySchema{
74+
Attributes: []hclext.AttributeSchema{
75+
{
76+
Name: providerTagsAttributeName,
77+
},
78+
},
79+
},
80+
},
81+
},
82+
}
83+
84+
providerBody, err := runner.GetProviderContent("aws", providerSchema, nil)
85+
if err != nil {
86+
return nil
87+
}
88+
89+
// Get provider default tags
90+
var providerAlias string
91+
for _, provider := range providerBody.Blocks.OfType("provider") {
92+
// Get the alias attribute, in terraform when there is a single aws provider its called "default"
93+
providerAttr, ok := provider.Body.Attributes["alias"]
94+
if !ok {
95+
providerAlias = "default"
96+
} else {
97+
err := runner.EvaluateExpr(providerAttr.Expr, func(alias string) error {
98+
providerAlias = alias
99+
return nil
100+
}, nil)
101+
if err != nil {
102+
return nil
103+
}
104+
}
105+
logger.Debug("Walk `%s` provider", providerAlias)
106+
107+
if len(provider.Body.Blocks) == 0 {
108+
var issue string
109+
if providerAlias == "default" {
110+
issue = "The aws provider is missing the `default_tags` block"
111+
} else {
112+
issue = fmt.Sprintf("The aws provider with alias `%s` is missing the `default_tags` block", providerAlias)
113+
}
114+
if err := runner.EmitIssue(r, issue, provider.DefRange); err != nil {
115+
return err
116+
}
117+
continue
118+
}
119+
120+
for _, block := range provider.Body.Blocks {
121+
var providerTags []string
122+
attr, ok := block.Body.Attributes[providerTagsAttributeName]
123+
if !ok {
124+
if err := r.emitIssue(runner, providerTags, config, provider.DefRange); err != nil {
125+
return err
126+
}
127+
continue
128+
}
129+
130+
err := runner.EvaluateExpr(attr.Expr, func(val cty.Value) error {
131+
keys, known := getKeysForValue(val)
132+
133+
if !known {
134+
logger.Warn("The missing aws tags rule can only evaluate provided variables, skipping %s.", provider.Labels[0]+"."+providerAlias+"."+providerDefaultTagsBlockName+"."+providerTagsAttributeName)
135+
return nil
136+
}
137+
138+
logger.Debug("Walk `%s` provider with tags `%v`", providerAlias, keys)
139+
providerTags = keys
140+
return nil
141+
}, nil)
142+
143+
if err != nil {
144+
return nil
145+
}
146+
147+
// Check tags
148+
if err := r.emitIssue(runner, providerTags, config, attr.Range); err != nil {
149+
return err
150+
}
151+
}
152+
}
153+
154+
return nil
155+
}
156+
157+
func (r *AwsProviderMissingDefaultTagsRule) emitIssue(runner tflint.Runner, tags []string, config awsProviderTagsRuleConfig, location hcl.Range) error {
158+
var missing []string
159+
for _, tag := range config.Tags {
160+
if !slices.Contains(tags, tag) {
161+
missing = append(missing, fmt.Sprintf("%q", tag))
162+
}
163+
}
164+
if len(missing) > 0 {
165+
sort.Strings(missing)
166+
wanted := strings.Join(missing, ", ")
167+
issue := fmt.Sprintf("The provider is missing the following tags: %s.", wanted)
168+
if err := runner.EmitIssue(r, issue, location); err != nil {
169+
return err
170+
}
171+
}
172+
173+
return nil
174+
}

0 commit comments

Comments
 (0)