Skip to content

Commit 048c26c

Browse files
authored
terraform: Finalize variable values in Evaluator (#1445)
* go mod tidy * Finalize variable values in Evaluator
1 parent 40ef004 commit 048c26c

File tree

6 files changed

+106
-17
lines changed

6 files changed

+106
-17
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/apparentlymart/go-cidr v1.1.0
99
github.com/bmatcuk/doublestar v1.1.5
1010
github.com/fatih/color v1.13.0
11+
github.com/go-test/deep v1.0.8
1112
github.com/golang/mock v1.6.0
1213
github.com/google/go-cmp v0.5.8
1314
github.com/google/go-github/v35 v35.3.0
@@ -43,7 +44,6 @@ require (
4344
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
4445
github.com/aws/aws-sdk-go v1.42.43 // indirect
4546
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
46-
github.com/go-test/deep v1.0.8 // indirect
4747
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
4848
github.com/golang/protobuf v1.5.2 // indirect
4949
github.com/google/go-querystring v1.0.0 // indirect

go.sum

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
8989
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
9090
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
9191
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
92-
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
9392
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
9493
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
9594
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=

terraform/evaluator.go

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/terraform-linters/tflint/terraform/lang"
1212
"github.com/terraform-linters/tflint/terraform/lang/marks"
1313
"github.com/zclconf/go-cty/cty"
14+
"github.com/zclconf/go-cty/cty/convert"
1415
)
1516

1617
type ContextMeta struct {
@@ -80,24 +81,33 @@ func (d *evaluationData) GetInputVariable(addr addrs.InputVariable, rng hcl.Rang
8081
return cty.UnknownVal(config.Type), diags
8182
}
8283

83-
// d.Evaluator.VariableValues should always contain valid "final values"
84-
// for variables, which is to say that they have already had type
85-
// conversions, validations, and default value handling applied to them.
86-
// Those are the responsibility of the graph notes representing the
87-
// variable declarations. Therefore here we just trust that we already
88-
// have a correct value.
89-
84+
// In Terraform, it is the responsibility of the VariableTransformer
85+
// to convert the variable to the "final value", including the type conversion.
86+
// However, since TFLint does not preprocess variables by Graph Builder,
87+
// type conversion and default value are applied by Evaluator as in Terraform v1.1.
88+
//
89+
// This has some restrictions on the representation of dynamic variables compared
90+
// to Terraform, but since TFLint is intended for static analysis, this is often enough.
9091
val, isSet := vals[addr.Name]
91-
if !isSet {
92-
// We should not be able to get here without having a valid value
93-
// for every variable, so this always indicates a bug in either
94-
// the graph builder (not including all the needed nodes) or in
95-
// the graph nodes representing variables.
92+
switch {
93+
case !isSet:
94+
// The config loader will ensure there is a default if the value is not
95+
// set at all.
96+
val = config.Default
97+
98+
case val.IsNull() && !config.Nullable && config.Default != cty.NilVal:
99+
// If nullable=false a null value will use the configured default.
100+
val = config.Default
101+
}
102+
103+
var err error
104+
val, err = convert.Convert(val, config.ConstraintType)
105+
if err != nil {
96106
diags = diags.Append(&hcl.Diagnostic{
97107
Severity: hcl.DiagError,
98-
Summary: `Reference to unresolved input variable`,
99-
Detail: `The final value is missing in Terraform's evaluation context. This is a bug in Terraform; please report it!`,
100-
Subject: rng.Ptr(),
108+
Summary: `Incorrect variable type`,
109+
Detail: fmt.Sprintf(`The resolved value of variable %q is not appropriate: %s.`, addr.Name, err),
110+
Subject: &config.DeclRange,
101111
})
102112
val = cty.UnknownVal(config.Type)
103113
}

terraform/variable.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Variable struct {
2323

2424
ParsingMode VariableParsingMode
2525
Sensitive bool
26+
Nullable bool
2627
}
2728

2829
func decodeVairableBlock(block *hclext.Block) (*Variable, hcl.Diagnostics) {
@@ -48,6 +49,15 @@ func decodeVairableBlock(block *hclext.Block) (*Variable, hcl.Diagnostics) {
4849
diags = diags.Extend(valDiags)
4950
}
5051

52+
if attr, exists := block.Body.Attributes["nullable"]; exists {
53+
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Nullable)
54+
diags = append(diags, valDiags...)
55+
} else {
56+
// The current default is true, which is subject to change in a future
57+
// language edition.
58+
v.Nullable = true
59+
}
60+
5161
if attr, exists := block.Body.Attributes["default"]; exists {
5262
val, valDiags := attr.Expr.Value(nil)
5363
diags = diags.Extend(valDiags)
@@ -210,5 +220,8 @@ var variableBlockSchema = &hclext.BodySchema{
210220
{
211221
Name: "sensitive",
212222
},
223+
{
224+
Name: "nullable",
225+
},
213226
},
214227
}

tflint/runner_eval_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,67 @@ resource "null_resource" "test" {
755755
},
756756
Expected: `cty.StringVal("p3.8xlarge")`,
757757
},
758+
{
759+
Name: "optional object attributes",
760+
Content: `
761+
variable "optional_object" {
762+
type = object({
763+
a = optional(string)
764+
b = optional(string)
765+
})
766+
}
767+
768+
resource "null_resource" "test" {
769+
key = coalesce(var.optional_object.b, "baz")
770+
}`,
771+
InputValues: []terraform.InputValues{
772+
{
773+
"optional_object": &terraform.InputValue{
774+
Value: cty.ObjectVal(map[string]cty.Value{"a": cty.StringVal("foo")}),
775+
},
776+
},
777+
},
778+
Expected: `cty.StringVal("baz")`,
779+
},
780+
{
781+
Name: "nullable value",
782+
Content: `
783+
variable "foo" {
784+
default = "bar"
785+
}
786+
787+
resource "null_resource" "test" {
788+
key = coalesce(var.foo, "baz")
789+
}`,
790+
InputValues: []terraform.InputValues{
791+
{
792+
"foo": &terraform.InputValue{
793+
Value: cty.NullVal(cty.String),
794+
},
795+
},
796+
},
797+
Expected: `cty.StringVal("baz")`,
798+
},
799+
{
800+
Name: "non-nullable value",
801+
Content: `
802+
variable "foo" {
803+
nullable = false
804+
default = "bar"
805+
}
806+
807+
resource "null_resource" "test" {
808+
key = coalesce(var.foo, "baz")
809+
}`,
810+
InputValues: []terraform.InputValues{
811+
{
812+
"foo": &terraform.InputValue{
813+
Value: cty.NullVal(cty.String),
814+
},
815+
},
816+
},
817+
Expected: `cty.StringVal("bar")`,
818+
},
758819
}
759820

760821
for _, tc := range cases {

tflint/runner_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func Test_NewModuleRunners_nestedModules(t *testing.T) {
5050
Name: "override",
5151
Default: cty.StringVal("foo"),
5252
Type: cty.DynamicPseudoType,
53+
Nullable: true,
5354
ParsingMode: terraform.VariableParseLiteral,
5455
DeclRange: hcl.Range{
5556
Filename: filepath.Join("module", "module.tf"),
@@ -61,6 +62,7 @@ func Test_NewModuleRunners_nestedModules(t *testing.T) {
6162
Name: "no_default",
6263
Default: cty.StringVal("bar"),
6364
Type: cty.DynamicPseudoType,
65+
Nullable: true,
6466
ParsingMode: terraform.VariableParseLiteral,
6567
DeclRange: hcl.Range{
6668
Filename: filepath.Join("module", "module.tf"),
@@ -72,6 +74,7 @@ func Test_NewModuleRunners_nestedModules(t *testing.T) {
7274
Name: "unknown",
7375
Default: cty.UnknownVal(cty.DynamicPseudoType),
7476
Type: cty.DynamicPseudoType,
77+
Nullable: true,
7578
ParsingMode: terraform.VariableParseLiteral,
7679
DeclRange: hcl.Range{
7780
Filename: filepath.Join("module", "module.tf"),
@@ -85,6 +88,7 @@ func Test_NewModuleRunners_nestedModules(t *testing.T) {
8588
Name: "override",
8689
Default: cty.StringVal("foo"),
8790
Type: cty.DynamicPseudoType,
91+
Nullable: true,
8892
ParsingMode: terraform.VariableParseLiteral,
8993
DeclRange: hcl.Range{
9094
Filename: filepath.Join("module", "module1", "resource.tf"),
@@ -96,6 +100,7 @@ func Test_NewModuleRunners_nestedModules(t *testing.T) {
96100
Name: "no_default",
97101
Default: cty.StringVal("bar"),
98102
Type: cty.DynamicPseudoType,
103+
Nullable: true,
99104
ParsingMode: terraform.VariableParseLiteral,
100105
DeclRange: hcl.Range{
101106
Filename: filepath.Join("module", "module1", "resource.tf"),
@@ -107,6 +112,7 @@ func Test_NewModuleRunners_nestedModules(t *testing.T) {
107112
Name: "unknown",
108113
Default: cty.UnknownVal(cty.DynamicPseudoType),
109114
Type: cty.DynamicPseudoType,
115+
Nullable: true,
110116
ParsingMode: terraform.VariableParseLiteral,
111117
DeclRange: hcl.Range{
112118
Filename: filepath.Join("module", "module1", "resource.tf"),

0 commit comments

Comments
 (0)