Skip to content

Commit c41e0d8

Browse files
committed
Add Resource Invalid Tags rule
1 parent 51d759d commit c41e0d8

4 files changed

+692
-25
lines changed

rules/aws_resource_invalid_tags.go

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
package rules
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
"strings"
7+
8+
hcl "github.com/hashicorp/hcl/v2"
9+
"github.com/terraform-linters/tflint-plugin-sdk/hclext"
10+
"github.com/terraform-linters/tflint-plugin-sdk/logger"
11+
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
12+
"github.com/terraform-linters/tflint-ruleset-aws/aws"
13+
"github.com/terraform-linters/tflint-ruleset-aws/project"
14+
"github.com/terraform-linters/tflint-ruleset-aws/rules/tags"
15+
"github.com/zclconf/go-cty/cty"
16+
"golang.org/x/exp/slices"
17+
)
18+
19+
// AwsResourceInvalidTagsRule checks whether resources are tagged with valid values
20+
type AwsResourceInvalidTagsRule struct {
21+
tflint.DefaultRule
22+
}
23+
24+
type awsResourceInvalidTagsRuleConfig struct {
25+
Tags map[string][]string `hclext:"tags"`
26+
Exclude []string `hclext:"exclude,optional"`
27+
}
28+
29+
// NewAwsResourceInvalidTagsRule returns new rules for all resources that support tags
30+
func NewAwsResourceInvalidTagsRule() *AwsResourceInvalidTagsRule {
31+
return &AwsResourceInvalidTagsRule{}
32+
}
33+
34+
// Name returns the rule name
35+
func (r *AwsResourceInvalidTagsRule) Name() string {
36+
return "aws_resource_invalid_tags"
37+
}
38+
39+
// Enabled returns whether the rule is enabled by default
40+
func (r *AwsResourceInvalidTagsRule) Enabled() bool {
41+
return false
42+
}
43+
44+
// Severity returns the rule severity
45+
func (r *AwsResourceInvalidTagsRule) Severity() tflint.Severity {
46+
return tflint.NOTICE
47+
}
48+
49+
// Link returns the rule reference link
50+
func (r *AwsResourceInvalidTagsRule) Link() string {
51+
return project.ReferenceLink(r.Name())
52+
}
53+
54+
// Check checks resources for invalid tags
55+
func (r *AwsResourceInvalidTagsRule) Check(runner tflint.Runner) error {
56+
config := awsResourceInvalidTagsRuleConfig{}
57+
if err := runner.DecodeRuleConfig(r.Name(), &config); err != nil {
58+
return err
59+
}
60+
61+
providerTagsMap, err := r.getProviderLevelTags(runner)
62+
63+
if err != nil {
64+
return err
65+
}
66+
67+
for _, resourceType := range tags.Resources {
68+
// Skip this resource if its type is excluded in configuration
69+
if stringInSlice(resourceType, config.Exclude) {
70+
continue
71+
}
72+
73+
resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{
74+
Attributes: []hclext.AttributeSchema{
75+
{Name: tagsAttributeName},
76+
{Name: providerAttributeName},
77+
},
78+
}, nil)
79+
80+
if err != nil {
81+
return err
82+
}
83+
84+
if resources.IsEmpty() {
85+
continue
86+
}
87+
88+
for _, resource := range resources.Blocks {
89+
providerAlias := "default"
90+
91+
// Override the provider alias if defined
92+
if val, ok := resource.Body.Attributes[providerAttributeName]; ok {
93+
provider, diagnostics := aws.DecodeProviderConfigRef(val.Expr, "provider")
94+
if diagnostics.HasErrors() {
95+
logger.Error("error decoding provider: %w", diagnostics)
96+
return diagnostics
97+
}
98+
providerAlias = provider.Alias
99+
}
100+
101+
providerAliasTags := providerTagsMap[providerAlias]
102+
103+
// If the resource has a tags attribute
104+
if attribute, okResource := resource.Body.Attributes[tagsAttributeName]; okResource {
105+
logger.Debug(
106+
"Walk `%s` attribute",
107+
resource.Labels[0]+"."+resource.Labels[1]+"."+tagsAttributeName,
108+
)
109+
110+
err := runner.EvaluateExpr(attribute.Expr, func(val cty.Value) error {
111+
knownTags, known := getKnownForValue(val)
112+
if !known {
113+
logger.Warn("The invalid aws tags rule can only evaluate provided variables, skipping %s.", resource.Labels[0]+"."+resource.Labels[1]+"."+tagsAttributeName)
114+
return nil
115+
}
116+
117+
// merge the known tags with the provider tags to comply with the implementation
118+
// https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/resource-tagging#propagating-tags-to-all-resources
119+
for providerTagKey, providerTagValue := range providerAliasTags {
120+
if _, ok := knownTags[providerTagKey]; !ok {
121+
knownTags[providerTagKey] = providerTagValue
122+
}
123+
}
124+
125+
r.emitIssue(runner, knownTags, config, attribute.Expr.Range())
126+
return nil
127+
}, nil)
128+
129+
if err != nil {
130+
return err
131+
}
132+
} else {
133+
logger.Debug("Walk `%s` resource", resource.Labels[0]+"."+resource.Labels[1])
134+
r.emitIssue(runner, providerAliasTags, config, resource.DefRange)
135+
}
136+
}
137+
}
138+
139+
// Special handling for tags on aws_autoscaling_group resources
140+
if err := r.checkAwsAutoScalingGroups(runner, config); err != nil {
141+
return err
142+
}
143+
144+
return nil
145+
}
146+
147+
func (r *AwsResourceInvalidTagsRule) getProviderLevelTags(runner tflint.Runner) (awsProvidersTags, error) {
148+
providerSchema := &hclext.BodySchema{
149+
Attributes: []hclext.AttributeSchema{
150+
{
151+
Name: "alias",
152+
Required: false,
153+
},
154+
},
155+
Blocks: []hclext.BlockSchema{
156+
{
157+
Type: defaultTagsBlockName,
158+
Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: tagsAttributeName}}},
159+
},
160+
},
161+
}
162+
163+
providerBody, err := runner.GetProviderContent("aws", providerSchema, nil)
164+
if err != nil {
165+
return nil, err
166+
}
167+
168+
// Get provider default tags
169+
allProviderTags := make(awsProvidersTags)
170+
var providerAlias string
171+
172+
for _, provider := range providerBody.Blocks.OfType(providerAttributeName) {
173+
// Get the alias attribute, in terraform when there is a single aws provider its called "default"
174+
providerAttr, ok := provider.Body.Attributes["alias"]
175+
if !ok {
176+
providerAlias = "default"
177+
} else {
178+
err := runner.EvaluateExpr(providerAttr.Expr, func(alias string) error {
179+
logger.Debug("Walk `%s` provider", providerAlias)
180+
providerAlias = alias
181+
// Init the provider reference even if it doesn't have tags
182+
allProviderTags[alias] = nil
183+
return nil
184+
}, nil)
185+
if err != nil {
186+
return nil, err
187+
}
188+
}
189+
190+
for _, block := range provider.Body.Blocks {
191+
attr, ok := block.Body.Attributes[tagsAttributeName]
192+
if !ok {
193+
continue
194+
}
195+
196+
err := runner.EvaluateExpr(attr.Expr, func(val cty.Value) error {
197+
tags, known := getKnownForValue(val)
198+
199+
if !known {
200+
logger.Warn("The invalid aws tags rule can only evaluate provided variables, skipping %s.", provider.Labels[0]+"."+providerAlias+"."+defaultTagsBlockName+"."+tagsAttributeName)
201+
return nil
202+
}
203+
204+
logger.Debug("Walk `%s` provider with tags `%v`", providerAlias, tags)
205+
allProviderTags[providerAlias] = tags
206+
return nil
207+
}, nil)
208+
209+
if err != nil {
210+
return nil, err
211+
}
212+
}
213+
}
214+
return allProviderTags, nil
215+
}
216+
217+
// checkAwsAutoScalingGroups handles the special case for tags on AutoScaling Groups
218+
// See: https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/autoscaling_tags.go
219+
func (r *AwsResourceInvalidTagsRule) checkAwsAutoScalingGroups(runner tflint.Runner, config awsResourceInvalidTagsRuleConfig) error {
220+
resourceType := "aws_autoscaling_group"
221+
222+
// Skip autoscaling group check if its type is excluded in configuration
223+
if stringInSlice(resourceType, config.Exclude) {
224+
return nil
225+
}
226+
227+
resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{}, nil)
228+
if err != nil {
229+
return err
230+
}
231+
232+
for _, resource := range resources.Blocks {
233+
asgTagBlockTags, tagBlockLocation, err := r.checkAwsAutoScalingGroupsTag(runner, resource)
234+
if err != nil {
235+
return err
236+
}
237+
238+
asgTagsAttributeTags, tagsAttributeLocation, err := r.checkAwsAutoScalingGroupsTags(runner, resource)
239+
if err != nil {
240+
return err
241+
}
242+
243+
switch {
244+
case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) > 0:
245+
runner.EmitIssue(r, "Only tag block or tags attribute may be present, but found both", resource.DefRange)
246+
case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) == 0:
247+
r.emitIssue(runner, map[string]string{}, config, resource.DefRange)
248+
case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) == 0:
249+
tags := asgTagBlockTags
250+
location := tagBlockLocation
251+
r.emitIssue(runner, tags, config, location)
252+
case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) > 0:
253+
tags := asgTagsAttributeTags
254+
location := tagsAttributeLocation
255+
r.emitIssue(runner, tags, config, location)
256+
}
257+
}
258+
259+
return nil
260+
}
261+
262+
// checkAwsAutoScalingGroupsTag checks tag{} blocks on aws_autoscaling_group resources
263+
func (r *AwsResourceInvalidTagsRule) checkAwsAutoScalingGroupsTag(runner tflint.Runner, resourceBlock *hclext.Block) (map[string]string, hcl.Range, error) {
264+
tags := map[string]string{}
265+
266+
resources, err := runner.GetResourceContent("aws_autoscaling_group", &hclext.BodySchema{
267+
Blocks: []hclext.BlockSchema{
268+
{
269+
Type: tagBlockName,
270+
Body: &hclext.BodySchema{
271+
Attributes: []hclext.AttributeSchema{
272+
{Name: "key"},
273+
{Name: "value"},
274+
},
275+
},
276+
},
277+
},
278+
}, nil)
279+
if err != nil {
280+
return tags, hcl.Range{}, err
281+
}
282+
283+
for _, resource := range resources.Blocks {
284+
if resource.Labels[0] != resourceBlock.Labels[0] {
285+
continue
286+
}
287+
288+
for _, tag := range resource.Body.Blocks {
289+
keyAttribute, keyExists := tag.Body.Attributes["key"]
290+
if !keyExists {
291+
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)
292+
}
293+
294+
valueAttribute, valueExists := tag.Body.Attributes["value"]
295+
if !valueExists {
296+
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)
297+
}
298+
299+
err := runner.EvaluateExpr(keyAttribute.Expr, func(key string) error {
300+
return runner.EvaluateExpr(valueAttribute.Expr, func(value string) error {
301+
tags[key] = value
302+
return nil
303+
}, nil)
304+
}, nil)
305+
if err != nil {
306+
return tags, hcl.Range{}, err
307+
}
308+
}
309+
}
310+
311+
return tags, resourceBlock.DefRange, nil
312+
}
313+
314+
// checkAwsAutoScalingGroupsTag checks the tags attribute on aws_autoscaling_group resources
315+
func (r *AwsResourceInvalidTagsRule) checkAwsAutoScalingGroupsTags(runner tflint.Runner, resourceBlock *hclext.Block) (map[string]string, hcl.Range, error) {
316+
tags := map[string]string{}
317+
318+
resources, err := runner.GetResourceContent("aws_autoscaling_group", &hclext.BodySchema{
319+
Attributes: []hclext.AttributeSchema{
320+
{Name: tagsAttributeName},
321+
},
322+
}, nil)
323+
if err != nil {
324+
return tags, hcl.Range{}, err
325+
}
326+
327+
for _, resource := range resources.Blocks {
328+
if resource.Labels[0] != resourceBlock.Labels[0] {
329+
continue
330+
}
331+
332+
attribute, ok := resource.Body.Attributes[tagsAttributeName]
333+
if ok {
334+
wantType := cty.List(cty.Object(map[string]cty.Type{
335+
"key": cty.String,
336+
"value": cty.String,
337+
"propagate_at_launch": cty.Bool,
338+
}))
339+
err := runner.EvaluateExpr(attribute.Expr, func(asgTags []awsAutoscalingGroupTag) error {
340+
for _, tag := range asgTags {
341+
tags[tag.Key] = tag.Value
342+
}
343+
return nil
344+
}, &tflint.EvaluateExprOption{WantType: &wantType})
345+
if err != nil {
346+
return tags, attribute.Expr.Range(), err
347+
}
348+
return tags, attribute.Expr.Range(), nil
349+
}
350+
}
351+
352+
return tags, resourceBlock.DefRange, nil
353+
}
354+
355+
func (r *AwsResourceInvalidTagsRule) emitIssue(runner tflint.Runner, tags map[string]string, config awsResourceInvalidTagsRuleConfig, location hcl.Range) {
356+
// sort the tag names for deterministic output
357+
// only evaluate the given tags on the resource NOT the configured tags
358+
// the checking of tag presence should use the `aws_resource_missing_tags` lint rule
359+
tagsToMatch := sort.StringSlice{}
360+
for tagName := range tags {
361+
tagsToMatch = append(tagsToMatch, tagName)
362+
}
363+
tagsToMatch.Sort()
364+
365+
str := ""
366+
for _, tagName := range tagsToMatch {
367+
allowedValues, ok := config.Tags[tagName]
368+
// if the tag has a rule configuration then check
369+
if ok {
370+
valueProvided := tags[tagName]
371+
if !slices.Contains(allowedValues, valueProvided) {
372+
str = str + fmt.Sprintf("Received '%s' for tag '%s', expected one of '%s'. ", valueProvided, tagName, strings.Join(allowedValues, ","))
373+
}
374+
}
375+
}
376+
377+
if len(str) > 0 {
378+
runner.EmitIssue(r, strings.TrimSpace(str), location)
379+
}
380+
}

0 commit comments

Comments
 (0)