diff --git a/integrationtest/inspection/dynblock-unknown/.tflint.hcl b/integrationtest/inspection/dynblock-unknown/.tflint.hcl new file mode 100644 index 000000000..e19f589dd --- /dev/null +++ b/integrationtest/inspection/dynblock-unknown/.tflint.hcl @@ -0,0 +1,3 @@ +plugin "testing" { + enabled = true +} diff --git a/integrationtest/inspection/dynblock-unknown/main.tf b/integrationtest/inspection/dynblock-unknown/main.tf new file mode 100644 index 000000000..3384a0c42 --- /dev/null +++ b/integrationtest/inspection/dynblock-unknown/main.tf @@ -0,0 +1,69 @@ +variable "unknown_set" { + type = set(bool) +} + +variable "unknown_bool" { + type = bool +} + +resource "aws_s3_bucket" "main" { + lifecycle_rule { + enabled = true + } + + dynamic "lifecycle_rule" { + for_each = var.unknown_set + + content { + enabled = lifecycle_rule.value + } + } + + dynamic "lifecycle_rule" { + for_each = toset([var.unknown_bool]) + + content { + enabled = lifecycle_rule.value + } + } +} + +resource "aws_iam_role" "main" { + inline_policy { + name = "static" + } + + dynamic "inline_policy" { + for_each = toset(["foo", "bar"]) + + content { + name = inline_policy.value + } + } + + dynamic "inline_policy" { + for_each = var.unknown_set + + content { + name = inline_policy.value + } + } +} + +resource "testing_assertions" "main" { + equal "static" {} + + dynamic "equal" { + for_each = toset(["known_label"]) + iterator = it + labels = [it.value] + content {} + } + + dynamic "equal" { + for_each = toset(["unknown_label"]) + iterator = it + labels = ["${it.value}-${var.unknown_bool}"] + content {} + } +} diff --git a/integrationtest/inspection/dynblock-unknown/result.json b/integrationtest/inspection/dynblock-unknown/result.json new file mode 100644 index 000000000..f2745261b --- /dev/null +++ b/integrationtest/inspection/dynblock-unknown/result.json @@ -0,0 +1,145 @@ +{ + "issues": [ + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`lifecycle_rule` block found", + "range": { + "filename": "main.tf", + "start": { + "line": 10, + "column": 3 + }, + "end": { + "line": 10, + "column": 17 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`enabled` attribute found: true", + "range": { + "filename": "main.tf", + "start": { + "line": 11, + "column": 15 + }, + "end": { + "line": 11, + "column": 19 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`lifecycle_rule` block found", + "range": { + "filename": "main.tf", + "start": { + "line": 22, + "column": 3 + }, + "end": { + "line": 22, + "column": 27 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_iam_role_example", + "severity": "error", + "link": "" + }, + "message": "inline policy found", + "range": { + "filename": "main.tf", + "start": { + "line": 32, + "column": 3 + }, + "end": { + "line": 32, + "column": 16 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_iam_role_example", + "severity": "error", + "link": "" + }, + "message": "name is static", + "range": { + "filename": "main.tf", + "start": { + "line": 33, + "column": 12 + }, + "end": { + "line": 33, + "column": 20 + } + }, + "callers": [] + }, + { + "rule": { + "name": "testing_assertions_example", + "severity": "error", + "link": "" + }, + "message": "equal block found: label=static", + "range": { + "filename": "main.tf", + "start": { + "line": 54, + "column": 3 + }, + "end": { + "line": 54, + "column": 17 + } + }, + "callers": [] + }, + { + "rule": { + "name": "testing_assertions_example", + "severity": "error", + "link": "" + }, + "message": "equal block found: label=known_label", + "range": { + "filename": "main.tf", + "start": { + "line": 56, + "column": 3 + }, + "end": { + "line": 56, + "column": 18 + } + }, + "callers": [] + } + ], + "errors": [] +} diff --git a/integrationtest/inspection/dynblock/result.json b/integrationtest/inspection/dynblock/result.json index 0ccb63ccc..b881f6141 100644 --- a/integrationtest/inspection/dynblock/result.json +++ b/integrationtest/inspection/dynblock/result.json @@ -26,7 +26,7 @@ "severity": "error", "link": "" }, - "message": "`enabled` attribute found", + "message": "`enabled` attribute found: false", "range": { "filename": "template.tf", "start": { @@ -60,6 +60,26 @@ }, "callers": [] }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`days` attribute found: 30", + "range": { + "filename": "template.tf", + "start": { + "line": 5, + "column": 23 + }, + "end": { + "line": 5, + "column": 25 + } + }, + "callers": [] + }, { "rule": { "name": "aws_s3_bucket_example_lifecycle_rule", @@ -70,12 +90,12 @@ "range": { "filename": "template.tf", "start": { - "line": 15, - "column": 5 + "line": 20, + "column": 3 }, "end": { - "line": 15, - "column": 12 + "line": 20, + "column": 27 } }, "callers": [] @@ -86,16 +106,76 @@ "severity": "error", "link": "" }, - "message": "`enabled` attribute found", + "message": "`lifecycle_rule` block found", "range": { "filename": "template.tf", "start": { - "line": 16, + "line": 20, + "column": 3 + }, + "end": { + "line": 20, + "column": 27 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`enabled` attribute found: false", + "range": { + "filename": "template.tf", + "start": { + "line": 24, "column": 17 }, "end": { - "line": 16, - "column": 37 + "line": 24, + "column": 65 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`enabled` attribute found: true", + "range": { + "filename": "template.tf", + "start": { + "line": 24, + "column": 17 + }, + "end": { + "line": 24, + "column": 65 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`transition` block found", + "range": { + "filename": "template.tf", + "start": { + "line": 26, + "column": 7 + }, + "end": { + "line": 26, + "column": 27 } }, "callers": [] @@ -110,12 +190,292 @@ "range": { "filename": "template.tf", "start": { - "line": 21, - "column": 9 + "line": 26, + "column": 7 + }, + "end": { + "line": 26, + "column": 27 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`transition` block found", + "range": { + "filename": "template.tf", + "start": { + "line": 26, + "column": 7 + }, + "end": { + "line": 26, + "column": 27 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`transition` block found", + "range": { + "filename": "template.tf", + "start": { + "line": 26, + "column": 7 + }, + "end": { + "line": 26, + "column": 27 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`days` attribute found: 30", + "range": { + "filename": "template.tf", + "start": { + "line": 30, + "column": 27 + }, + "end": { + "line": 30, + "column": 90 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`days` attribute found: 40", + "range": { + "filename": "template.tf", + "start": { + "line": 30, + "column": 27 + }, + "end": { + "line": 30, + "column": 90 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`days` attribute found: 60", + "range": { + "filename": "template.tf", + "start": { + "line": 30, + "column": 27 + }, + "end": { + "line": 30, + "column": 90 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`days` attribute found: 70", + "range": { + "filename": "template.tf", + "start": { + "line": 30, + "column": 27 + }, + "end": { + "line": 30, + "column": 90 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`lifecycle_rule` block found", + "range": { + "filename": "template.tf", + "start": { + "line": 46, + "column": 3 + }, + "end": { + "line": 46, + "column": 27 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`lifecycle_rule` block found", + "range": { + "filename": "template.tf", + "start": { + "line": 46, + "column": 3 + }, + "end": { + "line": 46, + "column": 27 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`lifecycle_rule` block found", + "range": { + "filename": "template.tf", + "start": { + "line": 46, + "column": 3 + }, + "end": { + "line": 46, + "column": 27 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`lifecycle_rule` block found", + "range": { + "filename": "template.tf", + "start": { + "line": 46, + "column": 3 + }, + "end": { + "line": 46, + "column": 27 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`enabled` attribute found: false", + "range": { + "filename": "template.tf", + "start": { + "line": 50, + "column": 17 + }, + "end": { + "line": 50, + "column": 51 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`enabled` attribute found: false", + "range": { + "filename": "template.tf", + "start": { + "line": 50, + "column": 17 + }, + "end": { + "line": 50, + "column": 51 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`enabled` attribute found: false", + "range": { + "filename": "template.tf", + "start": { + "line": 50, + "column": 17 + }, + "end": { + "line": 50, + "column": 51 + } + }, + "callers": [] + }, + { + "rule": { + "name": "aws_s3_bucket_example_lifecycle_rule", + "severity": "error", + "link": "" + }, + "message": "`enabled` attribute found: true", + "range": { + "filename": "template.tf", + "start": { + "line": 50, + "column": 17 }, "end": { - "line": 21, - "column": 16 + "line": 50, + "column": 51 } }, "callers": [] diff --git a/integrationtest/inspection/dynblock/template.tf b/integrationtest/inspection/dynblock/template.tf index 6ae07e759..b160109ef 100644 --- a/integrationtest/inspection/dynblock/template.tf +++ b/integrationtest/inspection/dynblock/template.tf @@ -9,20 +9,45 @@ resource "aws_s3_bucket" "bucket" { } resource "aws_s3_bucket" "dynamic" { + dynamic "cors_rule" { + for_each = toset(["*"]) + + content { + allowed_headers = [cors_rule.value] + } + } + dynamic "lifecycle_rule" { - for_each = toset([true]) + for_each = toset([true, false]) content { - enabled = lifecycle_rule.value + enabled = var.force_disable ? false : lifecycle_rule.value dynamic "transition" { - for_each = toset([30]) + for_each = toset([30, 60]) content { - days = transition.value + days = lifecycle_rule.value ? transition.value + 10 : transition.value storage_class = "STANDARD_IA" } } } } } + +variable "force_disable" { + type = bool + default = false +} + +resource "aws_s3_bucket" "dynamic_with_meta_arguments" { + for_each = toset([true, false]) + + dynamic "lifecycle_rule" { + for_each = toset([true, false]) + + content { + enabled = lifecycle_rule.value && each.value + } + } +} diff --git a/integrationtest/inspection/inspection_test.go b/integrationtest/inspection/inspection_test.go index 2eb803c75..d98d05228 100644 --- a/integrationtest/inspection/inspection_test.go +++ b/integrationtest/inspection/inspection_test.go @@ -118,6 +118,11 @@ func TestIntegration(t *testing.T) { Command: "./tflint --format json", Dir: "dynblock", }, + { + Name: "unknown dynamic blocks", + Command: "./tflint --format json", + Dir: "dynblock-unknown", + }, { Name: "provider config", Command: "./tflint --format json", diff --git a/plugin/server.go b/plugin/server.go index 8bd18dfe8..dfa90fa9f 100644 --- a/plugin/server.go +++ b/plugin/server.go @@ -115,9 +115,7 @@ func (s *GRPCServer) EvaluateExpr(expr hcl.Expression, opts sdk.EvaluateExprOpti runner = s.rootRunner } - // We always use EvalDataForNoInstanceKey here because an expression that depend on - // an instance key, such as `each.key` and `count.index`, is already bound. - val, diags := runner.Ctx.EvaluateExpr(expr, *opts.WantType, terraform.EvalDataForNoInstanceKey) + val, diags := runner.Ctx.EvaluateExpr(expr, *opts.WantType) if diags.HasErrors() { return val, diags } diff --git a/plugin/server_test.go b/plugin/server_test.go index 8d218d20f..076a345d6 100644 --- a/plugin/server_test.go +++ b/plugin/server_test.go @@ -60,6 +60,7 @@ resource "aws_instance" "bar" { Labels: []string{"aws_instance", "foo"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type"}}, + Blocks: hclext.Blocks{}, }, }, }, @@ -87,6 +88,7 @@ resource "aws_instance" "bar" { Labels: []string{"aws_instance", "bar"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type"}}, + Blocks: hclext.Blocks{}, }, }, }, @@ -114,6 +116,7 @@ resource "aws_instance" "bar" { Labels: []string{"aws_instance", "foo"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type"}}, + Blocks: hclext.Blocks{}, }, }, { @@ -121,6 +124,7 @@ resource "aws_instance" "bar" { Labels: []string{"aws_instance", "baz"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type"}}, + Blocks: hclext.Blocks{}, }, }, }, @@ -148,6 +152,7 @@ resource "aws_instance" "bar" { Labels: []string{"aws_instance", "foo"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type"}}, + Blocks: hclext.Blocks{}, }, }, { @@ -155,6 +160,7 @@ resource "aws_instance" "bar" { Labels: []string{"aws_instance", "baz"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type"}}, + Blocks: hclext.Blocks{}, }, }, }, diff --git a/plugin/stub-generator/sources/testing/main.go b/plugin/stub-generator/sources/testing/main.go index 275374e6f..8dcd78710 100644 --- a/plugin/stub-generator/sources/testing/main.go +++ b/plugin/stub-generator/sources/testing/main.go @@ -22,6 +22,8 @@ func main() { rules.NewAwsDBInstanceWithDefaultConfigExampleRule(), rules.NewAwsCloudFormationStackErrorRule(), rules.NewLocalsJustAttributesExampleRule(), + rules.NewAwsIAMRoleExampleRule(), + rules.NewTestingAssertionsExampleRule(), }, }, }) diff --git a/plugin/stub-generator/sources/testing/rules/aws_iam_role_example.go b/plugin/stub-generator/sources/testing/rules/aws_iam_role_example.go new file mode 100644 index 000000000..2f936da15 --- /dev/null +++ b/plugin/stub-generator/sources/testing/rules/aws_iam_role_example.go @@ -0,0 +1,87 @@ +package rules + +import ( + "fmt" + + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" +) + +// AwsIAMRoleExampleRule checks whether ... +type AwsIAMRoleExampleRule struct { + tflint.DefaultRule +} + +// NewAwsIAMPolicyExampleRule returns a new rule +func NewAwsIAMRoleExampleRule() *AwsIAMRoleExampleRule { + return &AwsIAMRoleExampleRule{} +} + +// Name returns the rule name +func (r *AwsIAMRoleExampleRule) Name() string { + return "aws_iam_role_example" +} + +// Enabled returns whether the rule is enabled by default +func (r *AwsIAMRoleExampleRule) Enabled() bool { + return true +} + +// Severity returns the rule severity +func (r *AwsIAMRoleExampleRule) Severity() tflint.Severity { + return tflint.ERROR +} + +// Link returns the rule reference link +func (r *AwsIAMRoleExampleRule) Link() string { + return "" +} + +// Check checks whether ... +func (r *AwsIAMRoleExampleRule) Check(runner tflint.Runner) error { + resources, err := runner.GetResourceContent("aws_iam_role", &hclext.BodySchema{ + Blocks: []hclext.BlockSchema{ + { + Type: "inline_policy", + Body: &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{{Name: "name"}}, + }, + }, + }, + }, &tflint.GetModuleContentOption{ + ModuleCtx: tflint.SelfModuleCtxType, + ExpandMode: tflint.ExpandModeNone, + }) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + for _, policy := range resource.Body.Blocks { + if err := runner.EmitIssue(r, "inline policy found", policy.DefRange); err != nil { + return err + } + + attribute, exists := policy.Body.Attributes["name"] + if !exists { + continue + } + + var name string + err := runner.EvaluateExpr(attribute.Expr, &name, nil) + + err = runner.EnsureNoError(err, func() error { + return runner.EmitIssue( + r, + fmt.Sprintf("name is %s", name), + attribute.Expr.Range(), + ) + }) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/plugin/stub-generator/sources/testing/rules/aws_s3_bucket_example_lifecycle_rule.go b/plugin/stub-generator/sources/testing/rules/aws_s3_bucket_example_lifecycle_rule.go index e0133e109..ab5bb5a20 100644 --- a/plugin/stub-generator/sources/testing/rules/aws_s3_bucket_example_lifecycle_rule.go +++ b/plugin/stub-generator/sources/testing/rules/aws_s3_bucket_example_lifecycle_rule.go @@ -1,6 +1,8 @@ package rules import ( + "fmt" + "github.com/terraform-linters/tflint-plugin-sdk/hclext" "github.com/terraform-linters/tflint-plugin-sdk/tflint" ) @@ -46,7 +48,14 @@ func (r *AwsS3BucketExampleLifecycleRuleRule) Check(runner tflint.Runner) error {Name: "enabled"}, }, Blocks: []hclext.BlockSchema{ - {Type: "transition", Body: &hclext.BodySchema{}}, + { + Type: "transition", + Body: &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{ + {Name: "days"}, + }, + }, + }, }, }, }, @@ -63,7 +72,11 @@ func (r *AwsS3BucketExampleLifecycleRuleRule) Check(runner tflint.Runner) error } if attr, exists := lifecycle.Body.Attributes["enabled"]; exists { - if err := runner.EmitIssue(r, "`enabled` attribute found", attr.Expr.Range()); err != nil { + var enabled string + err := runner.EnsureNoError(runner.EvaluateExpr(attr.Expr, &enabled, nil), func() error { + return runner.EmitIssue(r, fmt.Sprintf("`enabled` attribute found: %s", enabled), attr.Expr.Range()) + }) + if err != nil { return err } } @@ -72,6 +85,16 @@ func (r *AwsS3BucketExampleLifecycleRuleRule) Check(runner tflint.Runner) error if err := runner.EmitIssue(r, "`transition` block found", transition.DefRange); err != nil { return err } + + if attr, exists := transition.Body.Attributes["days"]; exists { + var days int + err := runner.EnsureNoError(runner.EvaluateExpr(attr.Expr, &days, nil), func() error { + return runner.EmitIssue(r, fmt.Sprintf("`days` attribute found: %d", days), attr.Expr.Range()) + }) + if err != nil { + return err + } + } } } } diff --git a/plugin/stub-generator/sources/testing/rules/testing_assertions_example.go b/plugin/stub-generator/sources/testing/rules/testing_assertions_example.go new file mode 100644 index 000000000..9a000dc32 --- /dev/null +++ b/plugin/stub-generator/sources/testing/rules/testing_assertions_example.go @@ -0,0 +1,63 @@ +package rules + +import ( + "fmt" + + "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint-plugin-sdk/tflint" +) + +// TestingAssertionsExampleRule checks whether ... +type TestingAssertionsExampleRule struct { + tflint.DefaultRule +} + +// NewTestingAssertionsExampleRule returns a new rule +func NewTestingAssertionsExampleRule() *TestingAssertionsExampleRule { + return &TestingAssertionsExampleRule{} +} + +// Name returns the rule name +func (r *TestingAssertionsExampleRule) Name() string { + return "testing_assertions_example" +} + +// Enabled returns whether the rule is enabled by default +func (r *TestingAssertionsExampleRule) Enabled() bool { + return true +} + +// Severity returns the rule severity +func (r *TestingAssertionsExampleRule) Severity() tflint.Severity { + return tflint.ERROR +} + +// Link returns the rule reference link +func (r *TestingAssertionsExampleRule) Link() string { + return "" +} + +// Check checks whether ... +func (r *TestingAssertionsExampleRule) Check(runner tflint.Runner) error { + resources, err := runner.GetResourceContent("testing_assertions", &hclext.BodySchema{ + Blocks: []hclext.BlockSchema{ + { + Type: "equal", + LabelNames: []string{"name"}, + }, + }, + }, nil) + if err != nil { + return err + } + + for _, resource := range resources.Blocks { + for _, equal := range resource.Body.Blocks { + if err := runner.EmitIssue(r, fmt.Sprintf("equal block found: label=%s", equal.Labels[0]), equal.DefRange); err != nil { + return err + } + } + } + + return nil +} diff --git a/terraform/evaluator.go b/terraform/evaluator.go index 978baea50..9e1ed56a6 100644 --- a/terraform/evaluator.go +++ b/terraform/evaluator.go @@ -8,6 +8,7 @@ import ( "github.com/agext/levenshtein" "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/hclext" "github.com/terraform-linters/tflint/terraform/addrs" "github.com/terraform-linters/tflint/terraform/lang" "github.com/terraform-linters/tflint/terraform/lang/marks" @@ -80,115 +81,62 @@ type Evaluator struct { CallStack *CallStack } -func (e *Evaluator) EvaluateExpr(expr hcl.Expression, wantType cty.Type, keyData InstanceKeyEvalData) (cty.Value, hcl.Diagnostics) { - scope := &lang.Scope{ - Data: &evaluationData{ - Evaluator: e, - ModulePath: e.ModulePath, - InstanceKeyData: keyData, - }, +// EvaluateExpr takes the given HCL expression and evaluates it to produce a value. +func (e *Evaluator) EvaluateExpr(expr hcl.Expression, wantType cty.Type) (cty.Value, hcl.Diagnostics) { + if e == nil { + panic("evaluator must not be nil") } - return scope.EvalExpr(expr, wantType) + return e.scope().EvalExpr(expr, wantType) } -type InstanceKeyEvalData struct { - CountIndex cty.Value - EachKey, EachValue cty.Value +// ExpandBlock expands "dynamic" blocks and resources/modules with count/for_each. +// +// In the expanded body, the content can be retrieved with the HCL API without +// being aware of the differences in the dynamic block schema. Also, the number +// of blocks and attribute values will be the same as the expanded result. +func (e *Evaluator) ExpandBlock(body hcl.Body, schema *hclext.BodySchema) (hcl.Body, hcl.Diagnostics) { + if e == nil { + return body, nil + } + return e.scope().ExpandBlock(body, schema) } -var EvalDataForNoInstanceKey = InstanceKeyEvalData{} +func (e *Evaluator) scope() *lang.Scope { + return &lang.Scope{ + Data: &evaluationData{ + Evaluator: e, + ModulePath: e.ModulePath, + }, + } +} type evaluationData struct { - Evaluator *Evaluator - ModulePath addrs.ModuleInstance - InstanceKeyData InstanceKeyEvalData + Evaluator *Evaluator + ModulePath addrs.ModuleInstance } var _ lang.Data = (*evaluationData)(nil) func (d *evaluationData) GetCountAttr(addr addrs.CountAttr, rng hcl.Range) (cty.Value, hcl.Diagnostics) { - var diags hcl.Diagnostics - - // Even when evaluating an expression that already has the value of `count.*` bound to it, - // it still tries to create an EvalContext because it contains `count.*` as a reference. - // In that case it returns an unknown value without returning an error. - if d.InstanceKeyData == EvalDataForNoInstanceKey { - return cty.UnknownVal(cty.Number), diags - } - - switch addr.Name { - - case "index": - idxVal := d.InstanceKeyData.CountIndex - if idxVal == cty.NilVal { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Reference to "count" in non-counted context`, - Detail: `The "count" object can only be used in "module", "resource", and "data" blocks, and only when the "count" argument is set.`, - Subject: rng.Ptr(), - }) - return cty.UnknownVal(cty.Number), diags - } - return idxVal, diags - - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Invalid "count" attribute`, - Detail: fmt.Sprintf(`The "count" object does not have an attribute named %q. The only supported attribute is count.index, which is the index of each instance of a resource block that has the "count" argument set.`, addr.Name), - Subject: rng.Ptr(), - }) - return cty.DynamicVal, diags - } + // Note that the actual evaluation of count.index is not done here. + // count.index is already evaluated when expanded by ExpandBlock, + // and the value is bound to the expanded body. + // + // Although, there are cases where count.index is evaluated as-is, + // such as when not expanding the body. In that case, evaluate it + // as an unknown and skip further checks. + return cty.UnknownVal(cty.Number), nil } func (d *evaluationData) GetForEachAttr(addr addrs.ForEachAttr, rng hcl.Range) (cty.Value, hcl.Diagnostics) { - var diags hcl.Diagnostics - - // Even when evaluating an expression that already has the value of `each.*` bound to it, - // it still tries to create an EvalContext because it contains `each.*` as a reference. - // In that case it returns an unknown value without returning an error. - if d.InstanceKeyData == EvalDataForNoInstanceKey { - return cty.UnknownVal(cty.DynamicPseudoType), diags - } - - var returnVal cty.Value - switch addr.Name { - - case "key": - returnVal = d.InstanceKeyData.EachKey - case "value": - returnVal = d.InstanceKeyData.EachValue - - if returnVal == cty.NilVal { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `each.value cannot be used in this context`, - Detail: `A reference to "each.value" has been used in a context in which it unavailable, such as when the configuration no longer contains the value in its "for_each" expression. Remove this reference to each.value in your configuration to work around this error.`, - Subject: rng.Ptr(), - }) - return cty.UnknownVal(cty.DynamicPseudoType), diags - } - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Invalid "each" attribute`, - Detail: fmt.Sprintf(`The "each" object does not have an attribute named %q. The supported attributes are each.key and each.value, the current key and value pair of the "for_each" attribute set.`, addr.Name), - Subject: rng.Ptr(), - }) - return cty.DynamicVal, diags - } - - if returnVal == cty.NilVal { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Reference to "each" in context without for_each`, - Detail: `The "each" object can be used only in "module" or "resource" blocks, and only when the "for_each" argument is set.`, - Subject: rng.Ptr(), - }) - return cty.UnknownVal(cty.DynamicPseudoType), diags - } - return returnVal, diags + // Note that the actual evaluation of each.key/each.value is not done here. + // each.key/each.value is already evaluated when expanded by ExpandBlock, + // and the value is bound to the expanded body. + // + // Although, there are cases where each.key/each.value is evaluated as-is, + // such as when not expanding the body. In that case, evaluate it + // as an unknown and skip further checks. + return cty.DynamicVal, nil } func (d *evaluationData) GetInputVariable(addr addrs.InputVariable, rng hcl.Range) (cty.Value, hcl.Diagnostics) { @@ -313,9 +261,7 @@ func (d *evaluationData) GetLocalValue(addr addrs.LocalValue, rng hcl.Range) (ct return cty.UnknownVal(cty.DynamicPseudoType), diags } - // Always use EvalDataForNoInstanceKey because local values cannot use expressions - // that depend on instance keys, such as `count.*` and `each.*`. - val, diags := d.Evaluator.EvaluateExpr(config.Expr, cty.DynamicPseudoType, EvalDataForNoInstanceKey) + val, diags := d.Evaluator.EvaluateExpr(config.Expr, cty.DynamicPseudoType) d.Evaluator.CallStack.Pop() return val, diags diff --git a/terraform/evaluator_test.go b/terraform/evaluator_test.go index b27f9a8bc..6795daf8f 100644 --- a/terraform/evaluator_test.go +++ b/terraform/evaluator_test.go @@ -38,7 +38,6 @@ func TestEvaluateExpr(t *testing.T) { inputs []InputValues expr hcl.Expression ty cty.Type - keyData InstanceKeyEvalData want string errCheck func(hcl.Diagnostics) bool }{ @@ -701,14 +700,6 @@ locals { want: `cty.UnknownVal(cty.Number)`, errCheck: neverHappend, }, - { - name: "count.index in counted context", - expr: expr(`count.index`), - ty: cty.Number, - keyData: InstanceKeyEvalData{CountIndex: cty.NumberIntVal(1)}, - want: `cty.NumberIntVal(1)`, - errCheck: neverHappend, - }, { name: "each.key in non-forEach context", expr: expr(`each.key`), @@ -716,14 +707,6 @@ locals { want: `cty.UnknownVal(cty.String)`, errCheck: neverHappend, }, - { - name: "each.key in forEach context", - expr: expr(`each.key`), - ty: cty.String, - keyData: InstanceKeyEvalData{EachKey: cty.StringVal("foo"), EachValue: cty.StringVal("bar")}, - want: `cty.StringVal("foo")`, - errCheck: neverHappend, - }, { name: "each.value in non-forEach context", expr: expr(`each.value`), @@ -732,15 +715,7 @@ locals { errCheck: neverHappend, }, { - name: "each.value in forEach context", - expr: expr(`each.value`), - ty: cty.String, - keyData: InstanceKeyEvalData{EachKey: cty.StringVal("foo"), EachValue: cty.StringVal("bar")}, - want: `cty.StringVal("bar")`, - errCheck: neverHappend, - }, - { - name: "bound expr without key data", + name: "bound expr", expr: hclext.BindValue(cty.StringVal("foo"), expr(`each.value`)), ty: cty.String, want: `cty.StringVal("foo")`, @@ -777,7 +752,7 @@ locals { CallStack: NewCallStack(), } - got, diags := evaluator.EvaluateExpr(test.expr, test.ty, test.keyData) + got, diags := evaluator.EvaluateExpr(test.expr, test.ty) if test.errCheck(diags) { t.Fatal(diags) } diff --git a/terraform/expandable.go b/terraform/expandable.go deleted file mode 100644 index ad6ad68e8..000000000 --- a/terraform/expandable.go +++ /dev/null @@ -1,208 +0,0 @@ -package terraform - -import ( - "fmt" - - "github.com/hashicorp/hcl/v2" - "github.com/terraform-linters/tflint-plugin-sdk/hclext" - "github.com/terraform-linters/tflint/terraform/addrs" - "github.com/terraform-linters/tflint/terraform/lang" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/gocty" -) - -type expandable struct { - Count hcl.Expression - ForEach hcl.Expression -} - -// expandBlock returns multiple blocks based on the meta-arguments (count/for_each). -// -// This function returns no blocks if `count` is 0, if `for_each` is empty, or if they are unknown. -// Otherwise it returns the number of blocks according to the value. -// -// Expressions containing `count.*` or `each.*` are evaluated here when expanding blocks. -// Make an instance key and bind the evaluation result based on it to the expression. -// Note that sensitive values are not bound. This is a limitation in value decoding. -// This means that `count.*`, `each.*` with sensitive values will resolve to unknown values. -func (e *expandable) expandBlock(ctx *Evaluator, block *hclext.Block) (hclext.Blocks, hcl.Diagnostics) { - if e.Count != nil { - return e.expandBlockByCount(ctx, block) - } - - if e.ForEach != nil { - return e.expandBlockByForEach(ctx, block) - } - - return hclext.Blocks{block}, hcl.Diagnostics{} -} - -func (e *expandable) expandBlockByCount(ctx *Evaluator, block *hclext.Block) (hclext.Blocks, hcl.Diagnostics) { - var diags hcl.Diagnostics - - countVal, countDiags := ctx.EvaluateExpr(e.Count, cty.Number, EvalDataForNoInstanceKey) - diags = diags.Extend(countDiags) - if diags.HasErrors() { - return hclext.Blocks{}, diags - } - countVal, _ = countVal.Unmark() - - if countVal.IsNull() { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid count argument", - Detail: `The given "count" argument value is null. An integer is required.`, - Subject: e.Count.Range().Ptr(), - }) - return hclext.Blocks{}, diags - } - if !countVal.IsKnown() { - // If count is unknown, no blocks are returned - return hclext.Blocks{}, diags - } - - var count int - err := gocty.FromCtyValue(countVal, &count) - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid count argument", - Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err), - Subject: e.Count.Range().Ptr(), - }) - return hclext.Blocks{}, diags - } - if count < 0 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid count argument", - Detail: `The given "count" argument value is unsuitable: negative numbers are not supported.`, - Subject: e.Count.Range().Ptr(), - }) - return hclext.Blocks{}, diags - } - - blocks := make(hclext.Blocks, count) - for i := 0; i < count; i++ { - expanded := block.Copy() - keyData := InstanceKeyEvalData{CountIndex: cty.NumberIntVal(int64(i))} - - walkDiags := expanded.Body.WalkAttributes(func(attr *hclext.Attribute) hcl.Diagnostics { - var diags hcl.Diagnostics - - refs, refsDiags := lang.ReferencesInExpr(attr.Expr) - if refsDiags.HasErrors() { - diags = diags.Extend(refsDiags) - return diags - } - - var contain bool - for _, ref := range refs { - if _, ok := ref.Subject.(addrs.CountAttr); ok { - contain = true - } - } - - if contain { - val, evalDiags := ctx.EvaluateExpr(attr.Expr, cty.DynamicPseudoType, keyData) - if evalDiags.HasErrors() { - diags = diags.Extend(evalDiags) - return diags - } - // If marked as sensitive, the cty.Value cannot be marshaled in MessagePack, - // so only bind it if it is unmarked. - if !val.ContainsMarked() { - // Even if there is no instance key later, the evaluated result is bound to - // the expression so that it can be referenced by EvaluateExpr. - attr.Expr = hclext.BindValue(val, attr.Expr) - } - } - return diags - }) - - diags = diags.Extend(walkDiags) - blocks[i] = expanded - } - - return blocks, diags -} - -func (e *expandable) expandBlockByForEach(ctx *Evaluator, block *hclext.Block) (hclext.Blocks, hcl.Diagnostics) { - var diags hcl.Diagnostics - - forEach, forEachDiags := ctx.EvaluateExpr(e.ForEach, cty.DynamicPseudoType, EvalDataForNoInstanceKey) - diags = diags.Extend(forEachDiags) - if diags.HasErrors() { - return hclext.Blocks{}, diags - } - - if forEach.IsNull() { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid for_each argument", - Detail: `The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A map, or set of strings is allowed.`, - Subject: e.ForEach.Range().Ptr(), - }) - return hclext.Blocks{}, diags - } - if !forEach.IsKnown() { - // If for_each is unknown, no blocks are returned - return hclext.Blocks{}, diags - } - if !forEach.CanIterateElements() { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "The `for_each` value is not iterable", - Detail: fmt.Sprintf("`%s` is not iterable", forEach.GoString()), - Subject: e.ForEach.Range().Ptr(), - }) - return hclext.Blocks{}, diags - } - - blocks := make(hclext.Blocks, forEach.LengthInt()) - it := forEach.ElementIterator() - for i := 0; it.Next(); i++ { - expanded := block.Copy() - - key, value := it.Element() - keyData := InstanceKeyEvalData{EachKey: key, EachValue: value} - - walkDiags := expanded.Body.WalkAttributes(func(attr *hclext.Attribute) hcl.Diagnostics { - var diags hcl.Diagnostics - - refs, refsDiags := lang.ReferencesInExpr(attr.Expr) - if refsDiags.HasErrors() { - diags = diags.Extend(refsDiags) - return diags - } - - var contain bool - for _, ref := range refs { - if _, ok := ref.Subject.(addrs.ForEachAttr); ok { - contain = true - } - } - - if contain { - val, evalDiags := ctx.EvaluateExpr(attr.Expr, cty.DynamicPseudoType, keyData) - if evalDiags.HasErrors() { - diags = diags.Extend(evalDiags) - return diags - } - // If marked as sensitive, the cty.Value cannot be marshaled in MessagePack, - // so only bind it if it is unmarked. - if !val.ContainsMarked() { - // Even if there is no instance key later, the evaluated result is bound to - // the expression so that it can be referenced by EvaluateExpr. - attr.Expr = hclext.BindValue(val, attr.Expr) - } - } - return diags - }) - - diags = diags.Extend(walkDiags) - blocks[i] = expanded - } - - return blocks, diags -} diff --git a/terraform/lang/eval.go b/terraform/lang/eval.go index 20cfbfcea..44a0ad982 100644 --- a/terraform/lang/eval.go +++ b/terraform/lang/eval.go @@ -4,12 +4,30 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/hclext" "github.com/terraform-linters/tflint/terraform/addrs" "github.com/terraform-linters/tflint/terraform/tfdiags" + "github.com/terraform-linters/tflint/terraform/tfhcl" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" ) +// ExpandBlock expands "dynamic" blocks and resources/modules with count/for_each. +// Note that Terraform only expands dynamic blocks, but TFLint also expands +// count/for_each here. +// +// Expressions in expanded blocks are evaluated immediately, so all variables +// contained in attributes specified in the body schema are gathered. +func (s *Scope) ExpandBlock(body hcl.Body, schema *hclext.BodySchema) (hcl.Body, hcl.Diagnostics) { + traversals := tfhcl.ExpandVariablesHCLExt(body, schema) + refs, diags := References(traversals) + + ctx, ctxDiags := s.EvalContext(refs) + diags = diags.Extend(ctxDiags) + + return tfhcl.Expand(body, ctx), diags +} + // EvalExpr evaluates a single expression in the receiving context and returns // the resulting value. The value will be converted to the given type before // it is returned if possible, or else an error diagnostic will be produced diff --git a/terraform/module.go b/terraform/module.go index 9f0660b3e..1c197d1f8 100644 --- a/terraform/module.go +++ b/terraform/module.go @@ -77,23 +77,23 @@ func (m *Module) build() hcl.Diagnostics { // Basically, this function is a wrapper for hclext.PartialContent, but in some ways it reproduces // Terraform language semantics. // -// 1. The block schema implicitly adds dynamic blocks to the target -// https://www.terraform.io/language/expressions/dynamic-blocks -// 2. Supports overriding files -// https://www.terraform.io/language/files/override +// 1. Supports overriding files +// https://developer.hashicorp.com/terraform/language/files/override +// 2. Expands "dynamic" blocks +// https://developer.hashicorp.com/terraform/language/expressions/dynamic-blocks // 3. Expands resource/module depends on the meta-arguments -// https://www.terraform.io/language/meta-arguments/count -// https://www.terraform.io/language/meta-arguments/for_each +// https://developer.hashicorp.com/terraform/language/meta-arguments/count +// https://developer.hashicorp.com/terraform/language/meta-arguments/for_each // -// But 3 won't run if you didn't pass the evaluation context. +// But 2 and 3 won't run if you didn't pass the evaluation context. func (m *Module) PartialContent(schema *hclext.BodySchema, ctx *Evaluator) (*hclext.BodyContent, hcl.Diagnostics) { content := &hclext.BodyContent{} diags := hcl.Diagnostics{} - schema = schemaWithDynamic(schema) - for _, f := range m.primaries { - c, d := hclext.PartialContent(f.Body, schema) + expanded, d := ctx.ExpandBlock(f.Body, schema) + diags = diags.Extend(d) + c, d := hclext.PartialContent(expanded, schema) diags = diags.Extend(d) for name, attr := range c.Attributes { content.Attributes[name] = attr @@ -101,7 +101,9 @@ func (m *Module) PartialContent(schema *hclext.BodySchema, ctx *Evaluator) (*hcl content.Blocks = append(content.Blocks, c.Blocks...) } for _, f := range m.overrides { - c, d := hclext.PartialContent(f.Body, schema) + expanded, d := ctx.ExpandBlock(f.Body, schema) + diags = diags.Extend(d) + c, d := hclext.PartialContent(expanded, schema) diags = diags.Extend(d) for name, attr := range c.Attributes { content.Attributes[name] = attr @@ -109,48 +111,9 @@ func (m *Module) PartialContent(schema *hclext.BodySchema, ctx *Evaluator) (*hcl content.Blocks = overrideBlocks(content.Blocks, c.Blocks) } - content = resolveDynamicBlocks(content) - - if ctx == nil { - return content, diags - } - - content, expandDiags := m.expandBlocks(content, ctx) - diags = diags.Extend(expandDiags) - return content, diags } -// expandBlocks expands resource/module blocks depending on evaluation context. -func (m *Module) expandBlocks(content *hclext.BodyContent, ctx *Evaluator) (*hclext.BodyContent, hcl.Diagnostics) { - out := &hclext.BodyContent{Attributes: content.Attributes} - diags := hcl.Diagnostics{} - - for _, block := range content.Blocks { - switch block.Type { - case "resource": - resourceType := block.Labels[0] - resourceName := block.Labels[1] - - resource := m.Resources[resourceType][resourceName] - blocks, expandDiags := resource.expandBlock(ctx, block) - diags = diags.Extend(expandDiags) - out.Blocks = append(out.Blocks, blocks...) - case "module": - name := block.Labels[0] - - module := m.ModuleCalls[name] - blocks, expandDiags := module.expandBlock(ctx, block) - diags = diags.Extend(expandDiags) - out.Blocks = append(out.Blocks, blocks...) - default: - out.Blocks = append(out.Blocks, block) - } - } - - return out, diags -} - // overrideBlocks changes the attributes in the passed primary blocks by override blocks recursively. func overrideBlocks(primaries, overrides hclext.Blocks) hclext.Blocks { dict := map[string]*hclext.Block{} @@ -172,58 +135,11 @@ func overrideBlocks(primaries, overrides hclext.Blocks) hclext.Blocks { return primaries } -// schemaWithDynamic appends a dynamic block schema to block schemes recursively. -// The content retrieved by the added schema is formatted by resolveDynamicBlocks in the same way as regular blocks. -func schemaWithDynamic(schema *hclext.BodySchema) *hclext.BodySchema { - out := &hclext.BodySchema{Mode: schema.Mode, Attributes: schema.Attributes} - - for _, block := range schema.Blocks { - block.Body = schemaWithDynamic(block.Body) - - out.Blocks = append(out.Blocks, block, hclext.BlockSchema{ - Type: "dynamic", - LabelNames: []string{"type"}, - Body: &hclext.BodySchema{ - Blocks: []hclext.BlockSchema{ - { - Type: "content", - Body: block.Body, - }, - }, - }, - }) - } - - return out -} - -// resolveDynamicBlocks formats the passed content based on the block schema added by schemaWithDynamic. -// This allows you to get all named blocks without being aware of the difference in the structure of the dynamic block. -func resolveDynamicBlocks(content *hclext.BodyContent) *hclext.BodyContent { - out := &hclext.BodyContent{Attributes: content.Attributes} - - for _, block := range content.Blocks { - block.Body = resolveDynamicBlocks(block.Body) - - if block.Type != "dynamic" { - out.Blocks = append(out.Blocks, block) - } else { - for _, dynamicContent := range block.Body.Blocks { - dynamicContent.Type = block.Labels[0] - out.Blocks = append(out.Blocks, dynamicContent) - } - } - } - - return out -} - var moduleSchema = &hclext.BodySchema{ Blocks: []hclext.BlockSchema{ { Type: "resource", LabelNames: []string{"type", "name"}, - Body: resourceBlockSchema, }, { Type: "variable", diff --git a/terraform/module_call.go b/terraform/module_call.go index 4e6d336b5..290254d5e 100644 --- a/terraform/module_call.go +++ b/terraform/module_call.go @@ -10,8 +10,6 @@ type ModuleCall struct { Name string SourceAddrRaw string - expandable - DeclRange hcl.Range } @@ -28,14 +26,6 @@ func decodeModuleBlock(block *hclext.Block) (*ModuleCall, hcl.Diagnostics) { diags = diags.Extend(valDiags) } - if attr, exists := block.Body.Attributes["count"]; exists { - mc.Count = attr.Expr - } - - if attr, exists := block.Body.Attributes["for_each"]; exists { - mc.ForEach = attr.Expr - } - return mc, diags } @@ -44,11 +34,5 @@ var moduleBlockSchema = &hclext.BodySchema{ { Name: "source", }, - { - Name: "count", - }, - { - Name: "for_each", - }, }, } diff --git a/terraform/module_test.go b/terraform/module_test.go index 59b03da56..ffa19d625 100644 --- a/terraform/module_test.go +++ b/terraform/module_test.go @@ -65,6 +65,7 @@ resource "aws_instance" "bar" { Labels: []string{"aws_instance", "foo"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main1.tf"}}}, + Blocks: hclext.Blocks{}, }, DefRange: hcl.Range{Filename: "main1.tf"}, }, @@ -73,6 +74,7 @@ resource "aws_instance" "bar" { Labels: []string{"aws_instance", "bar"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main2.tf"}}}, + Blocks: hclext.Blocks{}, }, DefRange: hcl.Range{Filename: "main2.tf"}, }, @@ -108,6 +110,7 @@ resource "aws_instance" "foo" { Labels: []string{"aws_instance", "foo"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main_override.tf"}}}, + Blocks: hclext.Blocks{}, }, DefRange: hcl.Range{Filename: "main.tf"}, }, @@ -140,6 +143,7 @@ locals { "foo": &hclext.Attribute{Name: "foo", Range: hcl.Range{Filename: "main.tf"}}, "bar": &hclext.Attribute{Name: "bar", Range: hcl.Range{Filename: "main.tf"}}, }, + Blocks: hclext.Blocks{}, }, DefRange: hcl.Range{Filename: "main.tf"}, }, @@ -286,6 +290,7 @@ resource "aws_instance" "bar" { Type: "ebs_block_device", Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"volume_size": &hclext.Attribute{Name: "volume_size", Range: hcl.Range{Filename: "main.tf"}}}, + Blocks: hclext.Blocks{}, }, DefRange: hcl.Range{Filename: "main.tf"}, }, @@ -303,6 +308,15 @@ resource "aws_instance" "bar" { Type: "ebs_block_device", Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"volume_size": &hclext.Attribute{Name: "volume_size", Range: hcl.Range{Filename: "main.tf"}}}, + Blocks: hclext.Blocks{}, + }, + DefRange: hcl.Range{Filename: "main.tf"}, + }, + { + Type: "ebs_block_device", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"volume_size": &hclext.Attribute{Name: "volume_size", Range: hcl.Range{Filename: "main.tf"}}}, + Blocks: hclext.Blocks{}, }, DefRange: hcl.Range{Filename: "main.tf"}, }, @@ -387,8 +401,8 @@ module "aws_instance" {} }, want: &hclext.BodyContent{ Blocks: hclext.Blocks{ - {Type: "resource", Labels: []string{"aws_instance", "main"}, Body: &hclext.BodyContent{Attributes: hclext.Attributes{}}}, - {Type: "module", Labels: []string{"aws_instance"}, Body: &hclext.BodyContent{Attributes: hclext.Attributes{}}}, + {Type: "resource", Labels: []string{"aws_instance", "main"}, Body: &hclext.BodyContent{Attributes: hclext.Attributes{}, Blocks: hclext.Blocks{}}}, + {Type: "module", Labels: []string{"aws_instance"}, Body: &hclext.BodyContent{Attributes: hclext.Attributes{}, Blocks: hclext.Blocks{}}}, }, }, }, @@ -1083,11 +1097,11 @@ module "aws_instance" { return i.DefRange.String() < j.DefRange.String() }), cmp.Comparer(func(x, y hcl.Expression) bool { - xv, diags := ctx.EvaluateExpr(x, cty.DynamicPseudoType, EvalDataForNoInstanceKey) + xv, diags := ctx.EvaluateExpr(x, cty.DynamicPseudoType) if diags.HasErrors() { t.Fatal(diags) } - yv, diags := ctx.EvaluateExpr(y, cty.DynamicPseudoType, EvalDataForNoInstanceKey) + yv, diags := ctx.EvaluateExpr(y, cty.DynamicPseudoType) if diags.HasErrors() { t.Fatal(diags) } @@ -1240,343 +1254,3 @@ func Test_overrideBlocks(t *testing.T) { }) } } - -func Test_schemaWithDynamic(t *testing.T) { - tests := []struct { - name string - in *hclext.BodySchema - want *hclext.BodySchema - }{ - { - name: "empty schema", - in: &hclext.BodySchema{}, - want: &hclext.BodySchema{}, - }, - { - name: "attribute schemas", - in: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{{Name: "foo"}}, - }, - want: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{{Name: "foo"}}, - }, - }, - { - name: "block schemas", - in: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{{Name: "foo"}}, - Blocks: []hclext.BlockSchema{ - { - Type: "toplevel", - Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "bar"}}}, - }, - }, - }, - want: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{{Name: "foo"}}, - Blocks: []hclext.BlockSchema{ - { - Type: "toplevel", - Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "bar"}}}, - }, - { - Type: "dynamic", - LabelNames: []string{"type"}, - Body: &hclext.BodySchema{ - Blocks: []hclext.BlockSchema{ - { - Type: "content", - Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "bar"}}}, - }, - }, - }, - }, - }, - }, - }, - { - name: "nested block schemas", - in: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{{Name: "foo"}}, - Blocks: []hclext.BlockSchema{ - { - Type: "toplevel", - Body: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{{Name: "bar"}}, - Blocks: []hclext.BlockSchema{ - { - Type: "nested", - Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "bar"}}}, - }, - }, - }, - }, - }, - }, - want: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{{Name: "foo"}}, - Blocks: []hclext.BlockSchema{ - { - Type: "toplevel", - Body: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{{Name: "bar"}}, - Blocks: []hclext.BlockSchema{ - { - Type: "nested", - Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "bar"}}}, - }, - { - Type: "dynamic", - LabelNames: []string{"type"}, - Body: &hclext.BodySchema{ - Blocks: []hclext.BlockSchema{ - { - Type: "content", - Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "bar"}}}, - }, - }, - }, - }, - }, - }, - }, - { - Type: "dynamic", - LabelNames: []string{"type"}, - Body: &hclext.BodySchema{ - Blocks: []hclext.BlockSchema{ - { - Type: "content", - Body: &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{{Name: "bar"}}, - Blocks: []hclext.BlockSchema{ - { - Type: "nested", - Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "bar"}}}, - }, - { - Type: "dynamic", - LabelNames: []string{"type"}, - Body: &hclext.BodySchema{ - Blocks: []hclext.BlockSchema{ - { - Type: "content", - Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "bar"}}}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got := schemaWithDynamic(test.in) - - if diff := cmp.Diff(got, test.want); diff != "" { - t.Error(diff) - } - }) - } -} - -func Test_resolveDynamicBlocks(t *testing.T) { - tests := []struct { - name string - in *hclext.BodyContent - want *hclext.BodyContent - }{ - { - name: "empty body", - in: &hclext.BodyContent{}, - want: &hclext.BodyContent{}, - }, - { - name: "only attributes", - in: &hclext.BodyContent{ - Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, - }, - want: &hclext.BodyContent{ - Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, - }, - }, - { - name: "regular blocks", - in: &hclext.BodyContent{ - Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, - Blocks: hclext.Blocks{ - { - Type: "toplevel", - Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"bar": &hclext.Attribute{Name: "bar"}}, - }, - }, - }, - }, - want: &hclext.BodyContent{ - Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, - Blocks: hclext.Blocks{ - { - Type: "toplevel", - Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"bar": &hclext.Attribute{Name: "bar"}}, - }, - }, - }, - }, - }, - { - name: "dynamic blocks", - in: &hclext.BodyContent{ - Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, - Blocks: hclext.Blocks{ - { - Type: "dynamic", - Labels: []string{"toplevel"}, - Body: &hclext.BodyContent{ - Blocks: hclext.Blocks{ - { - Type: "content", - Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"bar": &hclext.Attribute{Name: "bar"}}, - }, - }, - }, - }, - }, - }, - }, - want: &hclext.BodyContent{ - Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, - Blocks: hclext.Blocks{ - { - Type: "toplevel", - Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"bar": &hclext.Attribute{Name: "bar"}}, - }, - }, - }, - }, - }, - { - name: "dynamic nested blocks in regular blocks", - in: &hclext.BodyContent{ - Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, - Blocks: hclext.Blocks{ - { - Type: "toplevel", - Body: &hclext.BodyContent{ - Blocks: hclext.Blocks{ - { - Type: "dynamic", - Labels: []string{"nested"}, - Body: &hclext.BodyContent{ - Blocks: hclext.Blocks{ - { - Type: "content", - Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"bar": &hclext.Attribute{Name: "bar"}}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - want: &hclext.BodyContent{ - Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, - Blocks: hclext.Blocks{ - { - Type: "toplevel", - Body: &hclext.BodyContent{ - Blocks: hclext.Blocks{ - { - Type: "nested", - Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"bar": &hclext.Attribute{Name: "bar"}}, - }, - }, - }, - }, - }, - }, - }, - }, - { - name: "dynamic nested blocks in dynamic blocks", - in: &hclext.BodyContent{ - Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, - Blocks: hclext.Blocks{ - { - Type: "dynamic", - Labels: []string{"toplevel"}, - Body: &hclext.BodyContent{ - Blocks: hclext.Blocks{ - { - Type: "content", - Body: &hclext.BodyContent{ - Blocks: hclext.Blocks{ - { - Type: "dynamic", - Labels: []string{"nested"}, - Body: &hclext.BodyContent{ - Blocks: hclext.Blocks{ - { - Type: "content", - Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"bar": &hclext.Attribute{Name: "bar"}}, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - want: &hclext.BodyContent{ - Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, - Blocks: hclext.Blocks{ - { - Type: "toplevel", - Body: &hclext.BodyContent{ - Blocks: hclext.Blocks{ - { - Type: "nested", - Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"bar": &hclext.Attribute{Name: "bar"}}, - }, - }, - }, - }, - }, - }, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - got := resolveDynamicBlocks(test.in) - - if diff := cmp.Diff(got, test.want); diff != "" { - t.Error(diff) - } - }) - } -} diff --git a/terraform/resource.go b/terraform/resource.go index 4c9de88ef..f4ca3ab5e 100644 --- a/terraform/resource.go +++ b/terraform/resource.go @@ -9,32 +9,15 @@ type Resource struct { Name string Type string - expandable - DeclRange hcl.Range TypeRange hcl.Range } func decodeResourceBlock(block *hclext.Block) *Resource { - r := &Resource{ + return &Resource{ Type: block.Labels[0], Name: block.Labels[1], DeclRange: block.DefRange, TypeRange: block.LabelRanges[0], } - - if attr, exists := block.Body.Attributes["count"]; exists { - r.Count = attr.Expr - } - if attr, exists := block.Body.Attributes["for_each"]; exists { - r.ForEach = attr.Expr - } - return r -} - -var resourceBlockSchema = &hclext.BodySchema{ - Attributes: []hclext.AttributeSchema{ - {Name: "count"}, - {Name: "for_each"}, - }, } diff --git a/terraform/tfhcl/expand_body.go b/terraform/tfhcl/expand_body.go index 195a003e7..bc4d9e6e4 100644 --- a/terraform/tfhcl/expand_body.go +++ b/terraform/tfhcl/expand_body.go @@ -4,15 +4,17 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint-plugin-sdk/hclext" "github.com/zclconf/go-cty/cty" ) -// expandBody wraps another hcl.Body and expands any "dynamic" blocks found -// inside whenever Content or PartialContent is called. +// expandBody wraps another hcl.Body and expands any "dynamic" blocks, count/for-each +// resources found inside whenever Content or PartialContent is called. type expandBody struct { - original hcl.Body - forEachCtx *hcl.EvalContext - iteration *iteration // non-nil if we're nested inside another "dynamic" block + original hcl.Body + ctx *hcl.EvalContext + dynamicIteration *dynamicIteration // non-nil if we're nested inside a "dynamic" block + metaArgIteration *metaArgIteration // non-nil if we're nested inside a block with meta-arguments // These are used with PartialContent to produce a "remaining items" // body to return. They are nil on all bodies fresh out of the transformer. @@ -32,7 +34,8 @@ func (b *expandBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diag blocks, blockDiags := b.expandBlocks(schema, rawContent.Blocks, false) diags = append(diags, blockDiags...) - attrs := b.prepareAttributes(rawContent.Attributes) + attrs, attrDiags := b.prepareAttributes(rawContent.Attributes) + diags = append(diags, attrDiags...) content := &hcl.BodyContent{ Attributes: attrs, @@ -51,7 +54,8 @@ func (b *expandBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, h blocks, blockDiags := b.expandBlocks(schema, rawContent.Blocks, true) diags = append(diags, blockDiags...) - attrs := b.prepareAttributes(rawContent.Attributes) + attrs, attrDiags := b.prepareAttributes(rawContent.Attributes) + diags = append(diags, attrDiags...) content := &hcl.BodyContent{ Attributes: attrs, @@ -60,11 +64,12 @@ func (b *expandBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, h } remain := &expandBody{ - original: b.original, - forEachCtx: b.forEachCtx, - iteration: b.iteration, - hiddenAttrs: make(map[string]struct{}), - hiddenBlocks: make(map[string]hcl.BlockHeaderSchema), + original: b.original, + ctx: b.ctx, + dynamicIteration: b.dynamicIteration, + metaArgIteration: b.metaArgIteration, + hiddenAttrs: make(map[string]struct{}), + hiddenBlocks: make(map[string]hcl.BlockHeaderSchema), } for name := range b.hiddenAttrs { remain.hiddenAttrs[name] = struct{}{} @@ -118,10 +123,12 @@ func (b *expandBody) extendSchema(schema *hcl.BodySchema) *hcl.BodySchema { return extSchema } -func (b *expandBody) prepareAttributes(rawAttrs hcl.Attributes) hcl.Attributes { - if len(b.hiddenAttrs) == 0 && b.iteration == nil { +func (b *expandBody) prepareAttributes(rawAttrs hcl.Attributes) (hcl.Attributes, hcl.Diagnostics) { + var diags hcl.Diagnostics + + if len(b.hiddenAttrs) == 0 && b.dynamicIteration == nil && b.metaArgIteration == nil { // Easy path: just pass through the attrs from the original body verbatim - return rawAttrs + return rawAttrs, diags } // Otherwise we have some work to do: we must filter out any attributes @@ -133,11 +140,24 @@ func (b *expandBody) prepareAttributes(rawAttrs hcl.Attributes) hcl.Attributes { if _, hidden := b.hiddenAttrs[name]; hidden { continue } - if b.iteration != nil { + if b.dynamicIteration != nil || b.metaArgIteration != nil { attr := *rawAttr // shallow copy so we can mutate it - attr.Expr = exprWrap{ + expr := exprWrap{ Expression: attr.Expr, - i: b.iteration, + di: b.dynamicIteration, + mi: b.metaArgIteration, + } + // Unlike hcl/ext/dynblock, wrapped expressions are evaluated immediately. + // The result is bound to the expression and can be accessed without + // the iterator context. + val, evalDiags := expr.Value(b.ctx) + if evalDiags.HasErrors() { + diags = append(diags, evalDiags...) + continue + } + // Marked values (e.g. sensitive values) are unbound for serialization. + if !val.ContainsMarked() { + attr.Expr = hclext.BindValue(val, expr) } attrs[name] = &attr } else { @@ -145,7 +165,7 @@ func (b *expandBody) prepareAttributes(rawAttrs hcl.Attributes) hcl.Attributes { attrs[name] = rawAttr } } - return attrs + return attrs, diags } func (b *expandBody) expandBlocks(schema *hcl.BodySchema, rawBlocks hcl.Blocks, partial bool) (hcl.Blocks, hcl.Diagnostics) { @@ -180,39 +200,74 @@ func (b *expandBody) expandBlocks(schema *hcl.BodySchema, rawBlocks hcl.Blocks, continue } - spec, specDiags := b.decodeSpec(blockS, rawBlock) + spec, specDiags := b.decodeDynamicSpec(blockS, rawBlock) diags = append(diags, specDiags...) if specDiags.HasErrors() { continue } - if spec.forEachVal.IsKnown() { - for it := spec.forEachVal.ElementIterator(); it.Next(); { - key, value := it.Element() - i := b.iteration.MakeChild(spec.iteratorName, key, value) - - block, blockDiags := spec.newBlock(i, b.forEachCtx) - diags = append(diags, blockDiags...) - if block != nil { - // Attach our new iteration context so that attributes - // and other nested blocks can refer to our iterator. - block.Body = b.expandChild(block.Body, i) - blocks = append(blocks, block) - } - } - } else { - // If our top-level iteration value isn't known then we - // substitute an unknownBody, which will cause the entire block - // to evaluate to an unknown value. - i := b.iteration.MakeChild(spec.iteratorName, cty.DynamicVal, cty.DynamicVal) - block, blockDiags := spec.newBlock(i, b.forEachCtx) + if !spec.forEachVal.IsKnown() { + // If for_each is unknown, no blocks are returned + continue + } + + for it := spec.forEachVal.ElementIterator(); it.Next(); { + key, value := it.Element() + i := b.dynamicIteration.MakeChild(spec.iteratorName, key, value) + + block, blockDiags := spec.newBlock(i, b.ctx) diags = append(diags, blockDiags...) if block != nil { - block.Body = unknownBody{b.expandChild(block.Body, i)} + // Attach our new iteration context so that attributes + // and other nested blocks can refer to our iterator. + block.Body = b.expandChild(block.Body, i, b.metaArgIteration) blocks = append(blocks, block) } } + case "resource", "module": + if _, hidden := b.hiddenBlocks[rawBlock.Type]; hidden { + continue + } + + spec, specDiags := b.decodeMetaArgSpec(rawBlock) + diags = append(diags, specDiags...) + if specDiags.HasErrors() { + continue + } + + if spec.countSet { + if !spec.countVal.IsKnown() { + // If count is unknown, no blocks are returned + continue + } + + for idx := 0; idx < spec.countNum; idx++ { + i := MakeCountIteration(cty.NumberIntVal(int64(idx))) + + expandedBlock := *rawBlock // shallow copy + expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, i) + blocks = append(blocks, &expandedBlock) + } + } else if spec.forEachSet { + if !spec.forEachVal.IsKnown() { + // If for_each is unknown, no blocks are returned + continue + } + + for it := spec.forEachVal.ElementIterator(); it.Next(); { + i := MakeForEachIteration(it.Element()) + + expandedBlock := *rawBlock // shallow copy + expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, i) + blocks = append(blocks, &expandedBlock) + } + } else { + expandedBlock := *rawBlock + expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, b.metaArgIteration) + blocks = append(blocks, &expandedBlock) + } + default: if _, hidden := b.hiddenBlocks[rawBlock.Type]; !hidden { // A static block doesn't create a new iteration context, but @@ -220,7 +275,7 @@ func (b *expandBody) expandBlocks(schema *hcl.BodySchema, rawBlocks hcl.Blocks, // case it contains expressions that refer to our inherited // iterators, or nested "dynamic" blocks. expandedBlock := *rawBlock // shallow copy - expandedBlock.Body = b.expandChild(rawBlock.Body, b.iteration) + expandedBlock.Body = b.expandChild(rawBlock.Body, b.dynamicIteration, b.metaArgIteration) blocks = append(blocks, &expandedBlock) } } @@ -229,10 +284,11 @@ func (b *expandBody) expandBlocks(schema *hcl.BodySchema, rawBlocks hcl.Blocks, return blocks, diags } -func (b *expandBody) expandChild(child hcl.Body, i *iteration) hcl.Body { - chiCtx := i.EvalContext(b.forEachCtx) +func (b *expandBody) expandChild(child hcl.Body, i *dynamicIteration, mi *metaArgIteration) hcl.Body { + chiCtx := i.EvalContext(mi.EvalContext(b.ctx)) ret := Expand(child, chiCtx) - ret.(*expandBody).iteration = i + ret.(*expandBody).dynamicIteration = i + ret.(*expandBody).metaArgIteration = mi return ret } diff --git a/terraform/tfhcl/expand_body_test.go b/terraform/tfhcl/expand_body_test.go new file mode 100644 index 000000000..7fdd6878e --- /dev/null +++ b/terraform/tfhcl/expand_body_test.go @@ -0,0 +1,333 @@ +package tfhcl + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/hcl/v2/hcltest" + "github.com/zclconf/go-cty/cty" +) + +func TestExpand(t *testing.T) { + srcBody := hcltest.MockBody(&hcl.BodyContent{ + Blocks: hcl.Blocks{ + { + Type: "a", + Labels: []string{"static0"}, + LabelRanges: []hcl.Range{hcl.Range{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "val": hcltest.MockExprLiteral(cty.StringVal("static a 0")), + }), + }), + }, + { + Type: "b", + Body: hcltest.MockBody(&hcl.BodyContent{ + Blocks: hcl.Blocks{ + { + Type: "c", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "val0": hcltest.MockExprLiteral(cty.StringVal("static c 0")), + }), + }), + }, + { + Type: "dynamic", + Labels: []string{"c"}, + LabelRanges: []hcl.Range{hcl.Range{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ + cty.StringVal("dynamic c 0"), + cty.StringVal("dynamic c 1"), + })), + "iterator": hcltest.MockExprVariable("dyn_c"), + }), + Blocks: hcl.Blocks{ + { + Type: "content", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "val0": hcltest.MockExprTraversalSrc("dyn_c.value"), + }), + }), + }, + }, + }), + }, + }, + }), + }, + { + Type: "dynamic", + Labels: []string{"a"}, + LabelRanges: []hcl.Range{hcl.Range{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ + cty.StringVal("dynamic a 0"), + cty.StringVal("dynamic a 1"), + cty.StringVal("dynamic a 2"), + })), + "labels": hcltest.MockExprList([]hcl.Expression{ + hcltest.MockExprTraversalSrc("a.key"), + }), + }), + Blocks: hcl.Blocks{ + { + Type: "content", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "val": hcltest.MockExprTraversalSrc("a.value"), + }), + }), + }, + }, + }), + }, + { + Type: "dynamic", + Labels: []string{"b"}, + LabelRanges: []hcl.Range{hcl.Range{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ + cty.StringVal("dynamic b 0"), + cty.StringVal("dynamic b 1"), + })), + "iterator": hcltest.MockExprVariable("dyn_b"), + }), + Blocks: hcl.Blocks{ + { + Type: "content", + Body: hcltest.MockBody(&hcl.BodyContent{ + Blocks: hcl.Blocks{ + { + Type: "c", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "val0": hcltest.MockExprLiteral(cty.StringVal("static c 1")), + "val1": hcltest.MockExprTraversalSrc("dyn_b.value"), + }), + }), + }, + { + Type: "dynamic", + Labels: []string{"c"}, + LabelRanges: []hcl.Range{hcl.Range{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "for_each": hcltest.MockExprLiteral(cty.ListVal([]cty.Value{ + cty.StringVal("dynamic c 2"), + cty.StringVal("dynamic c 3"), + })), + }), + Blocks: hcl.Blocks{ + { + Type: "content", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "val0": hcltest.MockExprTraversalSrc("c.value"), + "val1": hcltest.MockExprTraversalSrc("dyn_b.value"), + }), + }), + }, + }, + }), + }, + }, + }), + }, + }, + }), + }, + { + Type: "dynamic", + Labels: []string{"b"}, + LabelRanges: []hcl.Range{hcl.Range{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "for_each": hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{ + "foo": cty.ListVal([]cty.Value{ + cty.StringVal("dynamic c nested 0"), + cty.StringVal("dynamic c nested 1"), + }), + })), + "iterator": hcltest.MockExprVariable("dyn_b"), + }), + Blocks: hcl.Blocks{ + { + Type: "content", + Body: hcltest.MockBody(&hcl.BodyContent{ + Blocks: hcl.Blocks{ + { + Type: "dynamic", + Labels: []string{"c"}, + LabelRanges: []hcl.Range{hcl.Range{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "for_each": hcltest.MockExprTraversalSrc("dyn_b.value"), + }), + Blocks: hcl.Blocks{ + { + Type: "content", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "val0": hcltest.MockExprTraversalSrc("c.value"), + "val1": hcltest.MockExprTraversalSrc("dyn_b.key"), + }), + }), + }, + }, + }), + }, + }, + }), + }, + }, + }), + }, + { + Type: "a", + Labels: []string{"static1"}, + LabelRanges: []hcl.Range{hcl.Range{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcltest.MockAttrs(map[string]hcl.Expression{ + "val": hcltest.MockExprLiteral(cty.StringVal("static a 1")), + }), + }), + }, + }, + }) + + dynBody := Expand(srcBody, nil) + var remain hcl.Body + + t.Run("PartialDecode", func(t *testing.T) { + decSpec := &hcldec.BlockMapSpec{ + TypeName: "a", + LabelNames: []string{"key"}, + Nested: &hcldec.AttrSpec{ + Name: "val", + Type: cty.String, + Required: true, + }, + } + + var got cty.Value + var diags hcl.Diagnostics + got, remain, diags = hcldec.PartialDecode(dynBody, decSpec, nil) + if len(diags) != 0 { + t.Errorf("unexpected diagnostics") + for _, diag := range diags { + t.Logf("- %s", diag) + } + return + } + + want := cty.MapVal(map[string]cty.Value{ + "static0": cty.StringVal("static a 0"), + "static1": cty.StringVal("static a 1"), + "0": cty.StringVal("dynamic a 0"), + "1": cty.StringVal("dynamic a 1"), + "2": cty.StringVal("dynamic a 2"), + }) + + if !got.RawEquals(want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + + t.Run("Decode", func(t *testing.T) { + decSpec := &hcldec.BlockListSpec{ + TypeName: "b", + Nested: &hcldec.BlockListSpec{ + TypeName: "c", + Nested: &hcldec.ObjectSpec{ + "val0": &hcldec.AttrSpec{ + Name: "val0", + Type: cty.String, + }, + "val1": &hcldec.AttrSpec{ + Name: "val1", + Type: cty.String, + }, + }, + }, + } + + var got cty.Value + var diags hcl.Diagnostics + got, diags = hcldec.Decode(remain, decSpec, nil) + if len(diags) != 0 { + t.Errorf("unexpected diagnostics") + for _, diag := range diags { + t.Logf("- %s", diag) + } + return + } + + want := cty.ListVal([]cty.Value{ + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("static c 0"), + "val1": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("dynamic c 0"), + "val1": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("dynamic c 1"), + "val1": cty.NullVal(cty.String), + }), + }), + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("static c 1"), + "val1": cty.StringVal("dynamic b 0"), + }), + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("dynamic c 2"), + "val1": cty.StringVal("dynamic b 0"), + }), + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("dynamic c 3"), + "val1": cty.StringVal("dynamic b 0"), + }), + }), + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("static c 1"), + "val1": cty.StringVal("dynamic b 1"), + }), + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("dynamic c 2"), + "val1": cty.StringVal("dynamic b 1"), + }), + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("dynamic c 3"), + "val1": cty.StringVal("dynamic b 1"), + }), + }), + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("dynamic c nested 0"), + "val1": cty.StringVal("foo"), + }), + cty.ObjectVal(map[string]cty.Value{ + "val0": cty.StringVal("dynamic c nested 1"), + "val1": cty.StringVal("foo"), + }), + }), + }) + + if !got.RawEquals(want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + +} diff --git a/terraform/tfhcl/expand_spec.go b/terraform/tfhcl/expand_spec.go index 2880ecf2d..4dbee278a 100644 --- a/terraform/tfhcl/expand_spec.go +++ b/terraform/tfhcl/expand_spec.go @@ -4,11 +4,13 @@ import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/terraform-linters/tflint/terraform/tfdiags" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" + "github.com/zclconf/go-cty/cty/gocty" ) -type expandSpec struct { +type expandDynamicSpec struct { blockType string blockTypeRange hcl.Range defRange hcl.Range @@ -16,10 +18,9 @@ type expandSpec struct { iteratorName string labelExprs []hcl.Expression contentBody hcl.Body - inherited map[string]*iteration } -func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Block) (*expandSpec, hcl.Diagnostics) { +func (b *expandBody) decodeDynamicSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Block) (*expandDynamicSpec, hcl.Diagnostics) { var diags hcl.Diagnostics var schema *hcl.BodySchema @@ -38,7 +39,7 @@ func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Bloc //// for_each attribute eachAttr := specContent.Attributes["for_each"] - eachVal, eachDiags := eachAttr.Expr.Value(b.forEachCtx) + eachVal, eachDiags := eachAttr.Expr.Value(b.ctx) diags = append(diags, eachDiags...) if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType { @@ -51,7 +52,7 @@ func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Bloc Detail: fmt.Sprintf("Cannot use a %s value in for_each. An iterable collection is required.", eachVal.Type().FriendlyName()), Subject: eachAttr.Expr.Range().Ptr(), Expression: eachAttr.Expr, - EvalContext: b.forEachCtx, + EvalContext: b.ctx, }) return nil, diags } @@ -62,7 +63,7 @@ func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Bloc Detail: "Cannot use a null value in for_each.", Subject: eachAttr.Expr.Range().Ptr(), Expression: eachAttr.Expr, - EvalContext: b.forEachCtx, + EvalContext: b.ctx, }) return nil, diags } @@ -139,7 +140,7 @@ func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Bloc return nil, diags } - return &expandSpec{ + return &expandDynamicSpec{ blockType: blockS.Type, blockTypeRange: rawSpec.LabelRanges[0], defRange: rawSpec.DefRange, @@ -150,7 +151,7 @@ func (b *expandBody) decodeSpec(blockS *hcl.BlockHeaderSchema, rawSpec *hcl.Bloc }, diags } -func (s *expandSpec) newBlock(i *iteration, ctx *hcl.EvalContext) (*hcl.Block, hcl.Diagnostics) { +func (s *expandDynamicSpec) newBlock(i *dynamicIteration, ctx *hcl.EvalContext) (*hcl.Block, hcl.Diagnostics) { var diags hcl.Diagnostics var labels []string var labelRanges []hcl.Range @@ -187,10 +188,13 @@ func (s *expandSpec) newBlock(i *iteration, ctx *hcl.EvalContext) (*hcl.Block, h return nil, diags } if !labelVal.IsKnown() { + return nil, diags + } + if labelVal.IsMarked() { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid dynamic block label", - Detail: "This value is not yet known. Dynamic block labels must be immediately-known values.", + Detail: "Cannot use a marked value as a dynamic block label.", Subject: labelExpr.Range().Ptr(), Expression: labelExpr, EvalContext: lCtx, @@ -213,3 +217,127 @@ func (s *expandSpec) newBlock(i *iteration, ctx *hcl.EvalContext) (*hcl.Block, h return block, diags } + +type expandMetaArgSpec struct { + rawBlock *hcl.Block + countSet bool + countVal cty.Value + countNum int + forEachSet bool + forEachVal cty.Value +} + +func (b *expandBody) decodeMetaArgSpec(rawSpec *hcl.Block) (*expandMetaArgSpec, hcl.Diagnostics) { + spec := &expandMetaArgSpec{rawBlock: rawSpec} + var diags hcl.Diagnostics + + specContent, _, specDiags := rawSpec.Body.PartialContent(expandableBlockBodySchema) + diags = append(diags, specDiags...) + if specDiags.HasErrors() { + return spec, diags + } + + //// count attribute + + if countAttr, exists := specContent.Attributes["count"]; exists { + spec.countSet = true + + countVal, countDiags := countAttr.Expr.Value(b.ctx) + diags = append(diags, countDiags...) + countVal, _ = countVal.Unmark() + + spec.countVal = countVal + + // We skip validation for count attribute if the value is unknwon + if countVal.IsKnown() { + if countVal.IsNull() { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid count argument", + Detail: `The given "count" argument value is null. An integer is required.`, + Subject: countAttr.Expr.Range().Ptr(), + Expression: countAttr.Expr, + EvalContext: b.ctx, + }) + return spec, diags + } + + var convErr error + countVal, convErr = convert.Convert(countVal, cty.Number) + if convErr != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Incorrect value type", + Detail: fmt.Sprintf("Invalid expression value: %s.", tfdiags.FormatError(convErr)), + Subject: countAttr.Expr.Range().Ptr(), + Expression: countAttr.Expr, + EvalContext: b.ctx, + }) + return spec, diags + } + + err := gocty.FromCtyValue(countVal, &spec.countNum) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid count argument", + Detail: fmt.Sprintf(`The given "count" argument value is unsuitable: %s.`, err), + Subject: countAttr.Expr.Range().Ptr(), + Expression: countAttr.Expr, + EvalContext: b.ctx, + }) + return spec, diags + } + if spec.countNum < 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid count argument", + Detail: `The given "count" argument value is unsuitable: negative numbers are not supported.`, + Subject: countAttr.Expr.Range().Ptr(), + Expression: countAttr.Expr, + EvalContext: b.ctx, + }) + return spec, diags + } + } + } + + //// for_each attribute + + if eachAttr, exists := specContent.Attributes["for_each"]; exists { + spec.forEachSet = true + + eachVal, eachDiags := eachAttr.Expr.Value(b.ctx) + diags = append(diags, eachDiags...) + + spec.forEachVal = eachVal + + if !eachVal.CanIterateElements() && eachVal.Type() != cty.DynamicPseudoType { + // We skip this error for DynamicPseudoType because that means we either + // have a null (which is checked immediately below) or an unknown + // (which is handled in the expandBody Content methods). + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "The `for_each` value is not iterable", + Detail: fmt.Sprintf("`%s` is not iterable", eachVal.GoString()), + Subject: eachAttr.Expr.Range().Ptr(), + Expression: eachAttr.Expr, + EvalContext: b.ctx, + }) + return spec, diags + } + if eachVal.IsNull() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid for_each argument", + Detail: `The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A map, or set of strings is allowed.`, + Subject: eachAttr.Expr.Range().Ptr(), + Expression: eachAttr.Expr, + EvalContext: b.ctx, + }) + return spec, diags + } + } + + return spec, diags +} diff --git a/terraform/tfhcl/expr_wrap.go b/terraform/tfhcl/expr_wrap.go index a2098b823..1843e4aef 100644 --- a/terraform/tfhcl/expr_wrap.go +++ b/terraform/tfhcl/expr_wrap.go @@ -7,22 +7,26 @@ import ( type exprWrap struct { hcl.Expression - i *iteration + di *dynamicIteration + mi *metaArgIteration } func (e exprWrap) Variables() []hcl.Traversal { raw := e.Expression.Variables() + if e.di == nil { + return raw + } ret := make([]hcl.Traversal, 0, len(raw)) - // Filter out traversals that refer to our iterator name or any + // Filter out traversals that refer to our dynamic iterator name or any // iterator we've inherited; we're going to provide those in // our Value wrapper, so the caller doesn't need to know about them. for _, traversal := range raw { rootName := traversal.RootName() - if rootName == e.i.IteratorName { + if rootName == e.di.IteratorName { continue } - if _, inherited := e.i.Inherited[rootName]; inherited { + if _, inherited := e.di.Inherited[rootName]; inherited { continue } ret = append(ret, traversal) @@ -31,7 +35,7 @@ func (e exprWrap) Variables() []hcl.Traversal { } func (e exprWrap) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { - extCtx := e.i.EvalContext(ctx) + extCtx := e.di.EvalContext(e.mi.EvalContext(ctx)) return e.Expression.Value(extCtx) } diff --git a/terraform/tfhcl/iteration.go b/terraform/tfhcl/iteration.go index 4e8bd29e4..e96dabb0b 100644 --- a/terraform/tfhcl/iteration.go +++ b/terraform/tfhcl/iteration.go @@ -5,30 +5,21 @@ import ( "github.com/zclconf/go-cty/cty" ) -type iteration struct { +type dynamicIteration struct { IteratorName string Key cty.Value Value cty.Value - Inherited map[string]*iteration + Inherited map[string]*dynamicIteration } -func (s *expandSpec) MakeIteration(key, value cty.Value) *iteration { - return &iteration{ - IteratorName: s.iteratorName, - Key: key, - Value: value, - Inherited: s.inherited, - } -} - -func (i *iteration) Object() cty.Value { +func (i *dynamicIteration) Object() cty.Value { return cty.ObjectVal(map[string]cty.Value{ "key": i.Key, "value": i.Value, }) } -func (i *iteration) EvalContext(base *hcl.EvalContext) *hcl.EvalContext { +func (i *dynamicIteration) EvalContext(base *hcl.EvalContext) *hcl.EvalContext { new := base.NewChild() if i != nil { @@ -42,25 +33,71 @@ func (i *iteration) EvalContext(base *hcl.EvalContext) *hcl.EvalContext { return new } -func (i *iteration) MakeChild(iteratorName string, key, value cty.Value) *iteration { +func (i *dynamicIteration) MakeChild(iteratorName string, key, value cty.Value) *dynamicIteration { if i == nil { // Create entirely new root iteration, then - return &iteration{ + return &dynamicIteration{ IteratorName: iteratorName, Key: key, Value: value, } } - inherited := map[string]*iteration{} + inherited := map[string]*dynamicIteration{} for name, otherIt := range i.Inherited { inherited[name] = otherIt } inherited[i.IteratorName] = i - return &iteration{ + return &dynamicIteration{ IteratorName: iteratorName, Key: key, Value: value, Inherited: inherited, } } + +type metaArgIteration struct { + Count bool + Index cty.Value + + ForEach bool + Key cty.Value + Value cty.Value +} + +func MakeCountIteration(index cty.Value) *metaArgIteration { + return &metaArgIteration{ + Count: true, + Index: index, + } +} + +func MakeForEachIteration(key, value cty.Value) *metaArgIteration { + return &metaArgIteration{ + ForEach: true, + Key: key, + Value: value, + } +} + +func (i *metaArgIteration) EvalContext(base *hcl.EvalContext) *hcl.EvalContext { + new := base.NewChild() + + if i != nil { + new.Variables = map[string]cty.Value{} + + if i.Count { + new.Variables["count"] = cty.ObjectVal(map[string]cty.Value{ + "index": i.Index, + }) + } + if i.ForEach { + new.Variables["each"] = cty.ObjectVal(map[string]cty.Value{ + "key": i.Key, + "value": i.Value, + }) + } + } + + return new +} diff --git a/terraform/tfhcl/public.go b/terraform/tfhcl/public.go index 0b92493e2..220a0e52f 100644 --- a/terraform/tfhcl/public.go +++ b/terraform/tfhcl/public.go @@ -1,10 +1,25 @@ +// Package tfhcl is a fork of hcl/ext/dynblock. +// Like dynblock, it supports dynamic block expansion, but also resource +// expansion via count/for_each meta-arguments. +// This package is defined separately from hclext because meta-arguments +// are a Terraform concern. package tfhcl import "github.com/hashicorp/hcl/v2" +// Expand "dynamic" blocks and count/for_for_each meta-arguments resources +// in the given body, returning a new body that has those blocks expanded. +// +// The given EvalContext is used when evaluating attributes within the given +// body. If the body has a dynamic block or an expandable resource, its +// contents are evaluated immediately. +// +// Expand returns no diagnostics because no blocks are actually expanded +// until a call to Content or PartialContent on the returned body, which +// will then expand only the blocks selected by the schema. func Expand(body hcl.Body, ctx *hcl.EvalContext) hcl.Body { return &expandBody{ - original: body, - forEachCtx: ctx, + original: body, + ctx: ctx, } } diff --git a/terraform/tfhcl/schema.go b/terraform/tfhcl/schema.go index 213f7b828..86c0fa024 100644 --- a/terraform/tfhcl/schema.go +++ b/terraform/tfhcl/schema.go @@ -48,3 +48,16 @@ var dynamicBlockBodySchemaNoLabels = &hcl.BodySchema{ }, }, } + +var expandableBlockBodySchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "count", + Required: false, + }, + { + Name: "for_each", + Required: false, + }, + }, +} diff --git a/terraform/tfhcl/unknown_body.go b/terraform/tfhcl/unknown_body.go deleted file mode 100644 index a6e7e246c..000000000 --- a/terraform/tfhcl/unknown_body.go +++ /dev/null @@ -1,89 +0,0 @@ -package tfhcl - -import ( - "github.com/hashicorp/hcl/v2" - "github.com/zclconf/go-cty/cty" -) - -// unknownBody is a funny body that just reports everything inside it as -// unknown. It uses a given other body as a sort of template for what attributes -// and blocks are inside -- including source location information -- but -// subsitutes unknown values of unknown type for all attributes. -// -// This rather odd process is used to handle expansion of dynamic blocks whose -// for_each expression is unknown. Since a block cannot itself be unknown, -// we instead arrange for everything _inside_ the block to be unknown instead, -// to give the best possible approximation. -type unknownBody struct { - template hcl.Body -} - -var _ hcl.Body = unknownBody{} - -// hcldec.UnkownBody impl -func (b unknownBody) Unknown() bool { - return true -} - -func (b unknownBody) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { - content, diags := b.template.Content(schema) - content = b.fixupContent(content) - - // We're intentionally preserving the diagnostics reported from the - // inner body so that we can still report where the template body doesn't - // match the requested schema. - return content, diags -} - -func (b unknownBody) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { - content, remain, diags := b.template.PartialContent(schema) - content = b.fixupContent(content) - remain = unknownBody{remain} // remaining content must also be wrapped - - // We're intentionally preserving the diagnostics reported from the - // inner body so that we can still report where the template body doesn't - // match the requested schema. - return content, remain, diags -} - -func (b unknownBody) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { - attrs, diags := b.template.JustAttributes() - attrs = b.fixupAttrs(attrs) - - // We're intentionally preserving the diagnostics reported from the - // inner body so that we can still report where the template body doesn't - // match the requested schema. - return attrs, diags -} - -func (b unknownBody) MissingItemRange() hcl.Range { - return b.template.MissingItemRange() -} - -func (b unknownBody) fixupContent(got *hcl.BodyContent) *hcl.BodyContent { - ret := &hcl.BodyContent{} - ret.Attributes = b.fixupAttrs(got.Attributes) - if len(got.Blocks) > 0 { - ret.Blocks = make(hcl.Blocks, 0, len(got.Blocks)) - for _, gotBlock := range got.Blocks { - new := *gotBlock // shallow copy - new.Body = unknownBody{gotBlock.Body} // nested content must also be marked unknown - ret.Blocks = append(ret.Blocks, &new) - } - } - - return ret -} - -func (b unknownBody) fixupAttrs(got hcl.Attributes) hcl.Attributes { - if len(got) == 0 { - return nil - } - ret := make(hcl.Attributes, len(got)) - for name, gotAttr := range got { - new := *gotAttr // shallow copy - new.Expr = hcl.StaticExpr(cty.DynamicVal, gotAttr.Expr.Range()) - ret[name] = &new - } - return ret -} diff --git a/terraform/tfhcl/variables_hclext.go b/terraform/tfhcl/variables_hclext.go new file mode 100644 index 000000000..6c80fc506 --- /dev/null +++ b/terraform/tfhcl/variables_hclext.go @@ -0,0 +1,65 @@ +package tfhcl + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/dynblock" + "github.com/terraform-linters/tflint-plugin-sdk/hclext" +) + +// ExpandVariablesHCLExt is a wrapper around dynblock.WalkVariables that +// uses the given hclext.BodySchema to automatically drive the recursive +// walk through nested blocks in the given body. +// +// Note that it's a wrapper around ExpandVariables, not WalkExpandVariables. +// This package evaluates expressions immediately on expansion, so we always +// need all variables to expand. It also implicitly walks count/for_each to +// support expansion by meta-arguments. +func ExpandVariablesHCLExt(body hcl.Body, schema *hclext.BodySchema) []hcl.Traversal { + rootNode := dynblock.WalkVariables(body) + return walkVariablesWithHCLExt(rootNode, schema) +} + +func walkVariablesWithHCLExt(node dynblock.WalkVariablesNode, schema *hclext.BodySchema) []hcl.Traversal { + vars, children := node.Visit(extendSchema(asHCLSchema(schema))) + + if len(children) > 0 { + childSchemas := childBlockTypes(schema) + for _, child := range children { + if childSchema, exists := childSchemas[child.BlockTypeName]; exists { + vars = append(vars, walkVariablesWithHCLExt(child.Node, childSchema.Body)...) + } + } + } + + return vars +} + +func asHCLSchema(in *hclext.BodySchema) *hcl.BodySchema { + out := &hcl.BodySchema{} + if in.Mode == hclext.SchemaJustAttributesMode { + return out + } + + out.Attributes = make([]hcl.AttributeSchema, len(in.Attributes)) + for idx, attr := range in.Attributes { + out.Attributes[idx] = hcl.AttributeSchema{Name: attr.Name, Required: attr.Required} + } + out.Blocks = make([]hcl.BlockHeaderSchema, len(in.Blocks)) + for idx, block := range in.Blocks { + out.Blocks[idx] = hcl.BlockHeaderSchema{Type: block.Type, LabelNames: block.LabelNames} + } + return out +} + +func extendSchema(schema *hcl.BodySchema) *hcl.BodySchema { + schema.Attributes = append(schema.Attributes, hcl.AttributeSchema{Name: "count"}, hcl.AttributeSchema{Name: "for_each"}) + return schema +} + +func childBlockTypes(schema *hclext.BodySchema) map[string]hclext.BlockSchema { + ret := make(map[string]hclext.BlockSchema) + for _, block := range schema.Blocks { + ret[block.Type] = block + } + return ret +} diff --git a/tflint/runner.go b/tflint/runner.go index 78ec9616a..9008201c3 100644 --- a/tflint/runner.go +++ b/tflint/runner.go @@ -117,7 +117,7 @@ func NewModuleRunners(parent *Runner) ([]*Runner, error) { modVars := map[string]*moduleVariable{} inputs := terraform.InputValues{} for varName, attribute := range body.Attributes { - val, diags := parent.Ctx.EvaluateExpr(attribute.Expr, cty.DynamicPseudoType, terraform.EvalDataForNoInstanceKey) + val, diags := parent.Ctx.EvaluateExpr(attribute.Expr, cty.DynamicPseudoType) if diags.HasErrors() { err := fmt.Errorf( "failed to eval an expression in %s:%d; %w", diff --git a/tflint/runner_test.go b/tflint/runner_test.go index f95ffc6a8..b8fd39d58 100644 --- a/tflint/runner_test.go +++ b/tflint/runner_test.go @@ -277,7 +277,7 @@ func Test_NewModuleRunners_withInvalidExpression(t *testing.T) { _, err := NewModuleRunners(runner) - expected := errors.New(`failed to eval an expression in module.tf:4; module.tf:4,16-29: Invalid "terraform" attribute; The terraform.env attribute was deprecated in v0.10 and removed in v0.12. The "state environment" concept was renamed to "workspace" in v0.12, and so the workspace name can now be accessed using the terraform.workspace attribute.`) + expected := errors.New(`module.tf:4,16-29: Invalid "terraform" attribute; The terraform.env attribute was deprecated in v0.10 and removed in v0.12. The "state environment" concept was renamed to "workspace" in v0.12, and so the workspace name can now be accessed using the terraform.workspace attribute.`) if err == nil { t.Fatal("an error was expected to occur, but it did not") }