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

Adds support for aws_autoscaling_group tag blocks and tags attributes #670

Merged
merged 3 commits into from
Mar 21, 2020
Merged
Show file tree
Hide file tree
Changes from 2 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
33 changes: 32 additions & 1 deletion docs/rules/aws_resource_missing_tags.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ Require specific tags for all AWS resource types that support them.
rule "aws_resource_missing_tags" {
enabled = true
tags = ["Foo", "Bar"]
exclude = ["aws_resource_missing_tags"] # (Optional) Exclude some resource types from tag checks
}
```

## Example
## Examples

Most resources use the `tags` attribute with simple `key`=`value` pairs:

```hcl
resource "aws_instance" "instance" {
Expand All @@ -36,6 +39,34 @@ Notice: aws_instance.instance is missing the following tags: "Bar", "Foo". (aws_
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.
Expand Down
3 changes: 2 additions & 1 deletion plugin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
hcl "github.com/hashicorp/hcl/v2"
tfplugin "github.com/terraform-linters/tflint-plugin-sdk/tflint"
"github.com/terraform-linters/tflint/tflint"
"github.com/zclconf/go-cty/cty"
)

// Server is a RPC server for responding to requests from plugins
Expand All @@ -29,7 +30,7 @@ func (s *Server) Attributes(req *tfplugin.AttributesRequest, resp *tfplugin.Attr

// EvalExpr returns a value of the evaluated expression
func (s *Server) EvalExpr(req *tfplugin.EvalExprRequest, resp *tfplugin.EvalExprResponse) error {
val, err := s.runner.EvalExpr(req.Expr, req.Ret)
val, err := s.runner.EvalExpr(req.Expr, req.Ret, cty.Type{})
if err != nil {
if appErr, ok := err.(*tflint.Error); ok {
err = tfplugin.Error(*appErr)
Expand Down
171 changes: 164 additions & 7 deletions rules/awsrules/aws_resource_missing_tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"strings"

hcl "github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/configs"
"github.com/terraform-linters/tflint/tflint"
"github.com/zclconf/go-cty/cty"
)

// AwsResourceMissingTagsRule checks whether the resource is tagged correctly
Expand All @@ -16,9 +18,15 @@ type AwsResourceMissingTagsRule struct {
}

type awsResourceTagsRuleConfig struct {
Tags []string `hcl:"tags"`
Tags []string `hcl:"tags"`
Exclude []string `hcl:"exclude,optional"`
}

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

// NewAwsResourceMissingTagsRule returns new rules for all resources that support tags
func NewAwsResourceMissingTagsRule() *AwsResourceMissingTagsRule {
resourceTypes := []string{
Expand Down Expand Up @@ -279,30 +287,41 @@ func (r *AwsResourceMissingTagsRule) Link() string {

// Check checks resources for missing tags
func (r *AwsResourceMissingTagsRule) Check(runner *tflint.Runner) error {
attributeName := "tags"
config := awsResourceTagsRuleConfig{}
if err := runner.DecodeRuleConfig(r.Name(), &config); err != nil {
return err
}

for _, resourceType := range r.resourceTypes {
// Skip this resource if its type is excluded in configuration
if stringInSlice(resourceType, config.Exclude) {
continue
}

// Special handling for tags on aws_autoscaling_group resources
if resourceType == "aws_autoscaling_group" {
r.checkAwsAutoScalingGroups(runner, config)
bwhaley marked this conversation as resolved.
Show resolved Hide resolved
continue
}

for _, resource := range runner.LookupResourcesByType(resourceType) {
body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: attributeName,
Name: tagsAttributeName,
},
},
})
if diags.HasErrors() {
return diags
}

if attribute, ok := body.Attributes[attributeName]; ok {
log.Printf("[DEBUG] Walk `%s` attribute", resource.Type+"."+resource.Name+"."+attributeName)
if attribute, ok := body.Attributes[tagsAttributeName]; ok {
log.Printf("[DEBUG] Walk `%s` attribute", resource.Type+"."+resource.Name+"."+tagsAttributeName)
err := runner.WithExpressionContext(attribute.Expr, func() error {
var resourceTags map[string]string
err := runner.EvaluateExpr(attribute.Expr, &resourceTags)
var err error
resourceTags := make(map[string]string)
err = runner.EvaluateExpr(attribute.Expr, &resourceTags)
return runner.EnsureNoError(err, func() error {
r.emitIssue(runner, resourceTags, config, attribute.Expr.Range())
return nil
Expand All @@ -320,6 +339,135 @@ 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 {
resourceType := "aws_autoscaling_group"

for _, resource := range runner.LookupResourcesByType(resourceType) {
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
}

var location hcl.Range
tags := make(map[string]string)
switch {
case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) > 0:
issue := fmt.Sprintf("Only tag block or tags attribute may be present, but found both")
runner.EmitIssue(r, issue, resource.DeclRange)
return nil
case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) == 0:
r.emitIssue(runner, map[string]string{}, config, resource.DeclRange)
return nil
case len(asgTagBlockTags) > 0 && len(asgTagsAttributeTags) == 0:
tags = asgTagBlockTags
location = tagBlockLocation
case len(asgTagBlockTags) == 0 && len(asgTagsAttributeTags) > 0:
tags = asgTagsAttributeTags
location = tagsAttributeLocation
}

return runner.EnsureNoError(err, func() error {
r.emitIssue(runner, tags, config, location)
return nil
})
}
return nil
}

// checkAwsAutoScalingGroupsTag checks tag{} blocks on aws_autoscaling_group resources
func (r *AwsResourceMissingTagsRule) checkAwsAutoScalingGroupsTag(runner *tflint.Runner, config awsResourceTagsRuleConfig, resource *configs.Resource) (map[string]string, hcl.Range, error) {
tags := make(map[string]string)
body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{
Blocks: []hcl.BlockHeaderSchema{
{
Type: tagBlockName,
},
},
})
if diags.HasErrors() {
return tags, (hcl.Range{}), diags
}

for _, tagBlock := range body.Blocks {
attributes, diags := tagBlock.Body.JustAttributes()
if diags.HasErrors() {
return tags, tagBlock.DefRange, diags
}

var key string
err := runner.EvaluateExpr(attributes["key"].Expr, &key)
bwhaley marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return tags, tagBlock.DefRange, diags
bwhaley marked this conversation as resolved.
Show resolved Hide resolved
}

var val string
err = runner.EvaluateExpr(attributes["value"].Expr, &val)
bwhaley marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return tags, tagBlock.DefRange, diags
}
tags[key] = val
}

return tags, resource.DeclRange, nil
}

// checkAwsAutoScalingGroupsTag checks the tags attribute on aws_autoscaling_group resources
func (r *AwsResourceMissingTagsRule) checkAwsAutoScalingGroupsTags(runner *tflint.Runner, config awsResourceTagsRuleConfig, resource *configs.Resource) (map[string]string, hcl.Range, error) {
tags := make(map[string]string)
body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: tagsAttributeName,
},
},
})
if diags.HasErrors() {
return tags, (hcl.Range{}), diags
}

attribute, ok := body.Attributes[tagsAttributeName]
if ok {
err := runner.WithExpressionContext(attribute.Expr, func() error {
wantType := cty.List(cty.Object(map[string]cty.Type{
"key": cty.String,
"value": cty.String,
"propagate_at_launch": cty.Bool,
}))
var asgTags []awsAutoscalingGroupTag
err := runner.EvaluateExprType(attribute.Expr, &asgTags, wantType)
if err != nil {
return err
}
for _, tag := range asgTags {
tags[tag.Key] = tag.Value
}
return nil
})
if err != nil {
return tags, attribute.Expr.Range(), err
}
return tags, attribute.Expr.Range(), nil
}

return tags, resource.DeclRange, nil
}

func (r *AwsResourceMissingTagsRule) emitIssue(runner *tflint.Runner, tags map[string]string, config awsResourceTagsRuleConfig, location hcl.Range) {
var missing []string
for _, tag := range config.Tags {
Expand All @@ -334,3 +482,12 @@ func (r *AwsResourceMissingTagsRule) emitIssue(runner *tflint.Runner, tags map[s
runner.EmitIssue(r, issue, location)
}
}

func stringInSlice(a string, list []string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
Loading