Skip to content

Commit bc357f5

Browse files
authored
Parse TF_VAR_* env variables as HCL (#1282)
1 parent e11a1f7 commit bc357f5

File tree

3 files changed

+142
-25
lines changed

3 files changed

+142
-25
lines changed

tflint/runner.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,17 @@ func NewRunner(c *Config, files map[string]*hcl.File, ants map[string]Annotation
4949
}
5050
log.Printf("[INFO] Initialize new runner for %s", path)
5151

52+
variableValues, diags := prepareVariableValues(cfg, variables...)
53+
if diags.HasErrors() {
54+
return nil, diags
55+
}
5256
ctx := terraform.BuiltinEvalContext{
5357
Evaluator: &terraform.Evaluator{
5458
Meta: &terraform.ContextMeta{
5559
Env: getTFWorkspace(),
5660
},
5761
Config: cfg.Root,
58-
VariableValues: prepareVariableValues(cfg, variables...),
62+
VariableValues: variableValues,
5963
VariableValuesLock: &sync.Mutex{},
6064
},
6165
}
@@ -358,8 +362,11 @@ func (r *Runner) listModuleVars(expr hcl.Expression) []*moduleVariable {
358362
// Therefore, CLI flag input variables must be passed at the end of arguments.
359363
// This is the responsibility of the caller.
360364
// See https://learn.hashicorp.com/terraform/getting-started/variables.html#assigning-variables
361-
func prepareVariableValues(config *configs.Config, variables ...terraform.InputValues) map[string]map[string]cty.Value {
365+
func prepareVariableValues(config *configs.Config, cliVars ...terraform.InputValues) (map[string]map[string]cty.Value, hcl.Diagnostics) {
362366
moduleKey := config.Path.UnkeyedInstanceShim().String()
367+
variableValues := make(map[string]map[string]cty.Value)
368+
variableValues[moduleKey] = make(map[string]cty.Value)
369+
363370
configVars := map[string]*configs.Variable{}
364371
for k, v := range config.Module.Variables {
365372
configVars[k] = v
@@ -369,14 +376,18 @@ func prepareVariableValues(config *configs.Config, variables ...terraform.InputV
369376
configVars[k].Default = cty.UnknownVal(v.Type)
370377
}
371378
}
372-
overrideVariables := terraform.DefaultVariableValues(configVars).Override(getTFEnvVariables()).Override(variables...)
373379

374-
variableValues := make(map[string]map[string]cty.Value)
375-
variableValues[moduleKey] = make(map[string]cty.Value)
380+
variables := terraform.DefaultVariableValues(configVars)
381+
envVars, diags := getTFEnvVariables(configVars)
382+
if diags.HasErrors() {
383+
return variableValues, diags
384+
}
385+
overrideVariables := variables.Override(envVars).Override(cliVars...)
386+
376387
for k, iv := range overrideVariables {
377388
variableValues[moduleKey][k] = iv.Value
378389
}
379-
return variableValues
390+
return variableValues, nil
380391
}
381392

382393
func listVarRefs(expr hcl.Expression) map[string]addrs.InputVariable {

tflint/terraform.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/hashicorp/hcl/v2/json"
1414
"github.com/terraform-linters/tflint/terraform/configs"
1515
"github.com/terraform-linters/tflint/terraform/terraform"
16-
"github.com/zclconf/go-cty/cty"
1716
)
1817

1918
var defaultValuesFile = "terraform.tfvars"
@@ -146,8 +145,10 @@ func getTFWorkspace() string {
146145
return current
147146
}
148147

149-
func getTFEnvVariables() terraform.InputValues {
148+
func getTFEnvVariables(declVars map[string]*configs.Variable) (terraform.InputValues, hcl.Diagnostics) {
150149
envVariables := make(terraform.InputValues)
150+
var diags hcl.Diagnostics
151+
151152
for _, e := range os.Environ() {
152153
idx := strings.Index(e, "=")
153154
envKey := e[:idx]
@@ -157,12 +158,26 @@ func getTFEnvVariables() terraform.InputValues {
157158
log.Printf("[INFO] TF_VAR_* environment variable found: key=%s", envKey)
158159
varName := strings.Replace(envKey, "TF_VAR_", "", 1)
159160

161+
var mode configs.VariableParsingMode
162+
declVar, declared := declVars[varName]
163+
if declared {
164+
mode = declVar.ParsingMode
165+
} else {
166+
mode = configs.VariableParseLiteral
167+
}
168+
169+
val, parseDiags := mode.Parse(varName, envVal)
170+
if parseDiags.HasErrors() {
171+
diags = diags.Extend(parseDiags)
172+
continue
173+
}
174+
160175
envVariables[varName] = &terraform.InputValue{
161-
Value: cty.StringVal(envVal),
176+
Value: val,
162177
SourceType: terraform.ValueFromEnvVar,
163178
}
164179
}
165180
}
166181

167-
return envVariables
182+
return envVariables, diags
168183
}

tflint/terraform_test.go

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -459,14 +459,18 @@ func Test_getTFWorkspace(t *testing.T) {
459459
func Test_getTFEnvVariables(t *testing.T) {
460460
cases := []struct {
461461
Name string
462+
DeclVars map[string]*configs.Variable
462463
EnvVar map[string]string
463464
Expected terraform.InputValues
464465
}{
465466
{
466-
Name: "environment variable",
467+
Name: "undeclared",
468+
DeclVars: map[string]*configs.Variable{},
467469
EnvVar: map[string]string{
468470
"TF_VAR_instance_type": "t2.micro",
469471
"TF_VAR_count": "5",
472+
"TF_VAR_list": "[\"foo\"]",
473+
"TF_VAR_map": "{foo=\"bar\"}",
470474
},
471475
Expected: terraform.InputValues{
472476
"instance_type": &terraform.InputValue{
@@ -477,28 +481,115 @@ func Test_getTFEnvVariables(t *testing.T) {
477481
Value: cty.StringVal("5"),
478482
SourceType: terraform.ValueFromEnvVar,
479483
},
484+
"list": &terraform.InputValue{
485+
Value: cty.StringVal("[\"foo\"]"),
486+
SourceType: terraform.ValueFromEnvVar,
487+
},
488+
"map": &terraform.InputValue{
489+
Value: cty.StringVal("{foo=\"bar\"}"),
490+
SourceType: terraform.ValueFromEnvVar,
491+
},
492+
},
493+
},
494+
{
495+
Name: "declared",
496+
DeclVars: map[string]*configs.Variable{
497+
"instance_type": {ParsingMode: configs.VariableParseLiteral},
498+
"count": {ParsingMode: configs.VariableParseHCL},
499+
"list": {ParsingMode: configs.VariableParseHCL},
500+
"map": {ParsingMode: configs.VariableParseHCL},
501+
},
502+
EnvVar: map[string]string{
503+
"TF_VAR_instance_type": "t2.micro",
504+
"TF_VAR_count": "5",
505+
"TF_VAR_list": "[\"foo\"]",
506+
"TF_VAR_map": "{foo=\"bar\"}",
507+
},
508+
Expected: terraform.InputValues{
509+
"instance_type": &terraform.InputValue{
510+
Value: cty.StringVal("t2.micro"),
511+
SourceType: terraform.ValueFromEnvVar,
512+
},
513+
"count": &terraform.InputValue{
514+
Value: cty.NumberIntVal(5),
515+
SourceType: terraform.ValueFromEnvVar,
516+
},
517+
"list": &terraform.InputValue{
518+
Value: cty.TupleVal([]cty.Value{cty.StringVal("foo")}),
519+
SourceType: terraform.ValueFromEnvVar,
520+
},
521+
"map": &terraform.InputValue{
522+
Value: cty.ObjectVal(map[string]cty.Value{"foo": cty.StringVal("bar")}),
523+
SourceType: terraform.ValueFromEnvVar,
524+
},
480525
},
481526
},
482527
}
483528

484529
for _, tc := range cases {
485-
for key, value := range tc.EnvVar {
486-
err := os.Setenv(key, value)
487-
if err != nil {
488-
t.Fatal(err)
530+
t.Run(tc.Name, func(t *testing.T) {
531+
for key, value := range tc.EnvVar {
532+
t.Setenv(key, value)
489533
}
490-
}
491534

492-
ret := getTFEnvVariables()
493-
if !reflect.DeepEqual(tc.Expected, ret) {
494-
t.Fatalf("Failed `%s` test:\n Expected: %#v\n Actual: %#v", tc.Name, tc.Expected, ret)
495-
}
535+
ret, diags := getTFEnvVariables(tc.DeclVars)
536+
if diags.HasErrors() {
537+
t.Fatal(diags)
538+
}
496539

497-
for key := range tc.EnvVar {
498-
err := os.Unsetenv(key)
499-
if err != nil {
500-
t.Fatal(err)
540+
opt := cmp.Comparer(func(x, y cty.Value) bool {
541+
return x.RawEquals(y)
542+
})
543+
if diff := cmp.Diff(tc.Expected, ret, opt); diff != "" {
544+
t.Error(diff)
501545
}
502-
}
546+
})
547+
}
548+
}
549+
550+
func Test_getTFEnvVariables_errors(t *testing.T) {
551+
cases := []struct {
552+
Name string
553+
DeclVars map[string]*configs.Variable
554+
Env map[string]string
555+
Expected string
556+
}{
557+
{
558+
Name: "invalid parsing mode",
559+
DeclVars: map[string]*configs.Variable{
560+
"foo": {ParsingMode: configs.VariableParseHCL},
561+
},
562+
Env: map[string]string{
563+
"TF_VAR_foo": "bar",
564+
},
565+
Expected: "<value for var.foo>:1,1-4: Variables not allowed; Variables may not be used here.",
566+
},
567+
{
568+
Name: "invalid expression",
569+
DeclVars: map[string]*configs.Variable{
570+
"foo": {ParsingMode: configs.VariableParseHCL},
571+
},
572+
Env: map[string]string{
573+
"TF_VAR_foo": `{"bar": "baz"`,
574+
},
575+
Expected: "<value for var.foo>:1,1-2: Unterminated object constructor expression; There is no corresponding closing brace before the end of the file. This may be caused by incorrect brace nesting elsewhere in this file.",
576+
},
577+
}
578+
579+
for _, tc := range cases {
580+
t.Run(tc.Name, func(t *testing.T) {
581+
for k, v := range tc.Env {
582+
t.Setenv(k, v)
583+
}
584+
585+
_, diags := getTFEnvVariables(tc.DeclVars)
586+
if !diags.HasErrors() {
587+
t.Fatal("Expected an error to occur, but it didn't")
588+
}
589+
590+
if diags.Error() != tc.Expected {
591+
t.Errorf("Expected `%s`, but got `%s`", tc.Expected, diags.Error())
592+
}
593+
})
503594
}
504595
}

0 commit comments

Comments
 (0)