Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added default tags functionality #489

Merged
merged 7 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aws/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ type ProviderConfigRef struct {
}

// original code: https://github.com/hashicorp/terraform/blob/3fbedf25430ead97eb42575d344427db3c32d524/internal/configs/resource.go#L498-L569
func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) {
func DecodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) {
var diags hcl.Diagnostics

var shimDiags hcl.Diagnostics
Expand Down
2 changes: 1 addition & 1 deletion aws/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func NewRunner(runner tflint.Runner, config *Config) (*Runner, error) {
func (r *Runner) AwsClient(attributes hclext.Attributes) (*Client, error) {
provider := "aws"
if attr, exists := attributes["provider"]; exists {
providerConfigRef, diags := decodeProviderConfigRef(attr.Expr, "provider")
providerConfigRef, diags := DecodeProviderConfigRef(attr.Expr, "provider")
if diags.HasErrors() {
logger.Error("parse resource provider attribute: %s", diags)
return nil, diags
Expand Down
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,15 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
)

require golang.org/x/net v0.10.0
require (
github.com/stretchr/testify v1.7.2
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53
golang.org/x/net v0.10.0
)

require (
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-hclog v1.5.0 // indirect
Expand All @@ -41,10 +46,12 @@ require (
github.com/kr/pretty v0.2.1 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.8.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o=
golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
Expand Down Expand Up @@ -154,6 +156,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
137 changes: 127 additions & 10 deletions rules/aws_resource_missing_tags.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package rules

import (
"errors"
"fmt"
"sort"
"strings"
Expand All @@ -9,9 +10,11 @@ import (
"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/maps"
)

// AwsResourceMissingTagsRule checks whether resources are tagged correctly
Expand All @@ -25,8 +28,9 @@ type awsResourceTagsRuleConfig struct {
}

const (
tagsAttributeName = "tags"
tagBlockName = "tag"
tagsAttributeName = "tags"
tagBlockName = "tag"
providerAttributeName = "provider"
)

// NewAwsResourceMissingTagsRule returns new rules for all resources that support tags
Expand Down Expand Up @@ -54,13 +58,81 @@ func (r *AwsResourceMissingTagsRule) Link() string {
return project.ReferenceLink(r.Name())
}

func (r *AwsResourceMissingTagsRule) getProviderLevelTags(runner tflint.Runner) (map[string]map[string]string, error) {
providerSchema := &hclext.BodySchema{
Attributes: []hclext.AttributeSchema{
{
Name: "alias",
Required: false,
},
},
Blocks: []hclext.BlockSchema{
{
Type: "default_tags",
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]map[string]string)
var providerAlias string
for _, provider := range providerBody.Blocks.OfType(providerAttributeName) {
providerTags := make(map[string]string)
for _, block := range provider.Body.Blocks {
attr, ok := block.Body.Attributes[tagsAttributeName]
if !ok {
continue
}

err := runner.EvaluateExpr(attr.Expr, func(tags map[string]string) error {
providerTags = tags
return nil
}, nil)

if err != nil {
return nil, err
}

// 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"
allProviderTags[providerAlias] = providerTags
} else {
err := runner.EvaluateExpr(providerAttr.Expr, func(alias string) error {
providerAlias = alias
return nil
}, nil)
// Assign default provider
allProviderTags[providerAlias] = providerTags
if err != nil {
return nil, err
}
}
}
}
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) {
Expand All @@ -77,29 +149,74 @@ func (r *AwsResourceMissingTagsRule) Check(runner tflint.Runner) error {
}

resources, err := runner.GetResourceContent(resourceType, &hclext.BodySchema{
Attributes: []hclext.AttributeSchema{{Name: tagsAttributeName}},
Attributes: []hclext.AttributeSchema{
{Name: tagsAttributeName},
{Name: providerAttributeName},
},
}, nil)
if err != nil {
return err
}

if resources.IsEmpty() {
continue
}

for _, resource := range resources.Blocks {
if attribute, ok := resource.Body.Attributes[tagsAttributeName]; ok {
logger.Debug("Walk `%s` attribute", resource.Labels[0]+"."+resource.Labels[1]+"."+tagsAttributeName)
wantType := cty.Map(cty.String)
err := runner.EvaluateExpr(attribute.Expr, func(resourceTags map[string]string) error {
r.emitIssue(runner, resourceTags, config, attribute.Expr.Range())
providerAlias := "default"
// Override the provider alias if defined
if val, ok := resource.Body.Attributes[providerAttributeName]; ok {
provider, diagnostics := aws.DecodeProviderConfigRef(val.Expr, "provider")
providerAlias = provider.Alias

if _, hasProvider := providerTagsMap[providerAlias]; !hasProvider {
errString := fmt.Sprintf(
"The aws provider with alias \"%s\" doesn't exist.",
providerAlias,
)
logger.Error("Error querying provider tags: %s", errString)
return errors.New(errString)
}

if diagnostics.HasErrors() {
logger.Error("error decoding provider: %w", diagnostics)
return diagnostics
}
}

resourceTags := make(map[string]string)

// The provider tags are to be overriden
// https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags
maps.Copy(resourceTags, 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,
)
// Since the evlaluateExpr, overrides k/v pairs, we need to re-copy the tags
resourceTagsAux := make(map[string]string)

err := runner.EvaluateExpr(attribute.Expr, func(val map[string]string) error {
resourceTagsAux = val
return nil
}, &tflint.EvaluateExprOption{WantType: &wantType})
}, nil)

maps.Copy(resourceTags, resourceTagsAux)
r.emitIssue(runner, resourceTags, config, attribute.Expr.Range())

if err != nil {
return err
}
} else {
logger.Debug("Walk `%s` resource", resource.Labels[0]+"."+resource.Labels[1])
r.emitIssue(runner, map[string]string{}, config, resource.DefRange)
r.emitIssue(runner, resourceTags, config, resource.DefRange)
}
}
}

return nil
}

Expand Down
Loading