diff --git a/docs/rules/aws_resource_missing_tags.md b/docs/rules/aws_resource_missing_tags.md index f54b0cca4..5f4d3991c 100644 --- a/docs/rules/aws_resource_missing_tags.md +++ b/docs/rules/aws_resource_missing_tags.md @@ -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" { @@ -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. diff --git a/plugin/server.go b/plugin/server.go index b2c2eba68..8b0ef15bf 100644 --- a/plugin/server.go +++ b/plugin/server.go @@ -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 @@ -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) diff --git a/rules/awsrules/aws_resource_missing_tags.go b/rules/awsrules/aws_resource_missing_tags.go index 6960de819..2b9188607 100644 --- a/rules/awsrules/aws_resource_missing_tags.go +++ b/rules/awsrules/aws_resource_missing_tags.go @@ -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 @@ -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{ @@ -279,18 +287,34 @@ 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" { + err := r.checkAwsAutoScalingGroups(runner, config) + err = runner.EnsureNoError(err, func() error { + return nil + }) + if err != nil { + return err + } + continue + } + for _, resource := range runner.LookupResourcesByType(resourceType) { body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ { - Name: attributeName, + Name: tagsAttributeName, }, }, }) @@ -298,11 +322,12 @@ func (r *AwsResourceMissingTagsRule) Check(runner *tflint.Runner) error { 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 @@ -320,6 +345,140 @@ 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 + } + + if _, ok := attributes["key"]; !ok { + err := &tflint.Error{ + Code: tflint.UnevaluableError, + Level: tflint.WarningLevel, + Message: fmt.Sprintf("Did not find expected field \"key\" in aws_autoscaling_group \"%s\" starting at line %d", + resource.Name, + resource.DeclRange.Start.Line, + ), + } + return tags, resource.DeclRange, err + } + + var key string + err := runner.EvaluateExpr(attributes["key"].Expr, &key) + if err != nil { + return tags, tagBlock.DefRange, err + } + tags[key] = "" + } + 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 { @@ -334,3 +493,13 @@ 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 +} + diff --git a/rules/awsrules/aws_resource_missing_tags.go.tmpl b/rules/awsrules/aws_resource_missing_tags.go.tmpl index e9d8b617d..42c3983de 100644 --- a/rules/awsrules/aws_resource_missing_tags.go.tmpl +++ b/rules/awsrules/aws_resource_missing_tags.go.tmpl @@ -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 @@ -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{ @@ -53,18 +61,34 @@ 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" { + err := r.checkAwsAutoScalingGroups(runner, config) + err = runner.EnsureNoError(err, func() error { + return nil + }) + if err != nil { + return err + } + continue + } + for _, resource := range runner.LookupResourcesByType(resourceType) { body, _, diags := resource.Config.PartialContent(&hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ { - Name: attributeName, + Name: tagsAttributeName, }, }, }) @@ -72,11 +96,12 @@ func (r *AwsResourceMissingTagsRule) Check(runner *tflint.Runner) error { 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 @@ -94,6 +119,140 @@ 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 + } + + if _, ok := attributes["key"]; !ok { + err := &tflint.Error{ + Code: tflint.UnevaluableError, + Level: tflint.WarningLevel, + Message: fmt.Sprintf("Did not find expected field \"key\" in aws_autoscaling_group \"%s\" starting at line %d", + resource.Name, + resource.DeclRange.Start.Line, + ), + } + return tags, resource.DeclRange, err + } + + var key string + err := runner.EvaluateExpr(attributes["key"].Expr, &key) + if err != nil { + return tags, tagBlock.DefRange, err + } + tags[key] = "" + } + 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 { @@ -108,3 +267,13 @@ 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 +} + diff --git a/rules/awsrules/aws_resource_missing_tags_test.go b/rules/awsrules/aws_resource_missing_tags_test.go index 52952df3d..4e031a594 100644 --- a/rules/awsrules/aws_resource_missing_tags_test.go +++ b/rules/awsrules/aws_resource_missing_tags_test.go @@ -9,7 +9,7 @@ import ( "github.com/terraform-linters/tflint/tflint" ) -func Test_AwsInstanceWithTags(t *testing.T) { +func Test_AwsResourceMissingTags(t *testing.T) { cases := []struct { Name string Content string @@ -83,6 +83,184 @@ rule "aws_resource_missing_tags" { }`, Expected: tflint.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: tflint.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: tflint.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: tflint.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: tflint.Issues{ + { + Rule: NewAwsResourceMissingTagsRule(), + Message: "The resource is missing the following tags: \"Bar\".", + Range: hcl.Range{ + Filename: "module.tf", + Start: hcl.Pos{Line: 3, Column: 12}, + 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: tflint.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: tflint.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: tflint.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}, + }, + }, + }, + }, } rule := NewAwsResourceMissingTagsRule() diff --git a/tflint/runner.go b/tflint/runner.go index dcf0ec8bd..23a555661 100644 --- a/tflint/runner.go +++ b/tflint/runner.go @@ -209,7 +209,7 @@ func NewModuleRunners(parent *Runner) ([]*Runner, error) { // EvalExpr is a wrapper of terraform.BultinEvalContext.EvaluateExpr // In addition, this method determines whether the expression is evaluable, contains no unknown values, and so on. // The returned cty.Value is converted according to the value passed as `ret`. -func (r *Runner) EvalExpr(expr hcl.Expression, ret interface{}) (cty.Value, error) { +func (r *Runner) EvalExpr(expr hcl.Expression, ret interface{}, wantType cty.Type) (cty.Value, error) { evaluable, err := isEvaluableExpr(expr) if err != nil { err := &Error{ @@ -240,22 +240,23 @@ func (r *Runner) EvalExpr(expr hcl.Expression, ret interface{}) (cty.Value, erro return cty.NullVal(cty.NilType), err } - var wantType cty.Type - switch ret.(type) { - case *string, string: - wantType = cty.String - case *int, int: - wantType = cty.Number - case *[]string, []string: - wantType = cty.List(cty.String) - case *[]int, []int: - wantType = cty.List(cty.Number) - case *map[string]string, map[string]string: - wantType = cty.Map(cty.String) - case *map[string]int, map[string]int: - wantType = cty.Map(cty.Number) - default: - panic(fmt.Errorf("Unexpected result type: %T", ret)) + if wantType == (cty.Type{}) { + switch ret.(type) { + case *string, string: + wantType = cty.String + case *int, int: + wantType = cty.Number + case *[]string, []string: + wantType = cty.List(cty.String) + case *[]int, []int: + wantType = cty.List(cty.Number) + case *map[string]string, map[string]string: + wantType = cty.Map(cty.String) + case *map[string]int, map[string]int: + wantType = cty.Map(cty.Number) + default: + panic(fmt.Errorf("Unexpected result type: %T", ret)) + } } val, diags := r.ctx.EvaluateExpr(expr, wantType, nil) @@ -316,12 +317,24 @@ func (r *Runner) EvalExpr(expr hcl.Expression, ret interface{}) (cty.Value, erro // EvaluateExpr evaluates the expression and reflects the result in the value of `ret`. // In the future, it will be no longer needed because all evaluation requests are invoked from RPC client func (r *Runner) EvaluateExpr(expr hcl.Expression, ret interface{}) error { - val, err := r.EvalExpr(expr, ret) + val, err := r.EvalExpr(expr, ret, cty.Type{}) if err != nil { return err } + return r.fromCtyValue(val, expr, ret) +} - err = gocty.FromCtyValue(val, ret) +// EvaluateExprType is like EvaluateExpr, but also accepts a known cty.Type to pass to EvalExpr +func (r *Runner) EvaluateExprType(expr hcl.Expression, ret interface{}, wantType cty.Type) error { + val, err := r.EvalExpr(expr, ret, wantType) + if err != nil { + return err + } + return r.fromCtyValue(val, expr, ret) +} + +func (r *Runner) fromCtyValue(val cty.Value, expr hcl.Expression, ret interface{}) error { + err := gocty.FromCtyValue(val, ret) if err != nil { err := &Error{ Code: TypeMismatchError,