Skip to content

Commit d2faa71

Browse files
core and backend: remove redundant handling of default variable values
Previously we had three different layers all thinking they were responsible for substituting a default value for an unset root module variable: - the local backend, via logic in backend.ParseVariableValues - the context.Plan function (and other similar functions) trying to preprocess the input variables using terraform.mergeDefaultInputVariableValues . - the newer prepareFinalInputVariableValue, which aims to centralize all of the variable preparation logic so it can be common to both root and child module variables. The second of these was also trying to handle type constraint checking, which is also the responsibility of the central function and not something we need to handle so early. Only the last of these consistently handles both root and child module variables, and so is the one we ought to keep. The others are now redundant and are causing prepareFinalInputVariableValue to get a slightly corrupted view of the caller's chosen variable values. To rectify that, here we remove the two redundant layers altogether and have unset root variables pass through as cty.NilVal all the way to the central prepareFinalInputVariableValue function, which will then handle them in a suitable way which properly respects the "nullable" setting. This commit includes some test changes in the terraform package to make those tests no longer rely on the mergeDefaultInputVariableValues logic we've removed, and to instead explicitly set cty.NilVal for all unset variables to comply with our intended contract for PlanOpts.SetVariables, and similar. (This is so that we can more easily catch bugs in callers where they _don't_ correctly handle input variables; it allows us to distinguish between the caller explicitly marking a variable as unset vs. not describing it at all, where the latter is a bug in the caller.)
1 parent 72bff83 commit d2faa71

14 files changed

+259
-306
lines changed

internal/backend/unparsed_value.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,18 @@ func ParseVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*
164164

165165
// By this point we should've gathered all of the required root module
166166
// variables from one of the many possible sources. We'll now populate
167-
// any we haven't gathered as their defaults and fail if any of the
168-
// missing ones are required.
167+
// any we haven't gathered as unset placeholders which Terraform Core
168+
// can then react to.
169169
for name, vc := range decls {
170170
if isDefinedAny(name, ret, undeclared) {
171171
continue
172172
}
173173

174+
// This check is redundant with a check made in Terraform Core when
175+
// processing undeclared variables, but allows us to generate a more
176+
// specific error message which mentions -var and -var-file command
177+
// line options, whereas the one in Terraform Core is more general
178+
// due to supporting both root and child module variables.
174179
if vc.Required() {
175180
diags = diags.Append(&hcl.Diagnostic{
176181
Severity: hcl.DiagError,
@@ -189,8 +194,14 @@ func ParseVariableValues(vv map[string]UnparsedVariableValue, decls map[string]*
189194
SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange),
190195
}
191196
} else {
197+
// We're still required to put an entry for this variable
198+
// in the mapping to be explicit to Terraform Core that we
199+
// visited it, but its value will be cty.NilVal to represent
200+
// that it wasn't set at all at this layer, and so Terraform Core
201+
// should substitute a default if available, or generate an error
202+
// if not.
192203
ret[name] = &terraform.InputValue{
193-
Value: vc.Default,
204+
Value: cty.NilVal,
194205
SourceType: terraform.ValueFromConfig,
195206
SourceRange: tfdiags.SourceRangeFromHCL(vc.DeclRange),
196207
}

internal/backend/unparsed_value_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ func TestUnparsedValue(t *testing.T) {
204204
},
205205
},
206206
"missing2": {
207-
Value: cty.StringVal("default for missing2"),
207+
Value: cty.NilVal, // Terraform Core handles substituting the default
208208
SourceType: terraform.ValueFromConfig,
209209
SourceRange: tfdiags.SourceRange{
210210
Filename: "fake.tf",

internal/command/jsonplan/plan.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func Marshal(
118118
output := newPlan()
119119
output.TerraformVersion = version.String()
120120

121-
err := output.marshalPlanVariables(p.VariableValues, schemas)
121+
err := output.marshalPlanVariables(p.VariableValues, config.Module.Variables)
122122
if err != nil {
123123
return nil, fmt.Errorf("error in marshalPlanVariables: %s", err)
124124
}
@@ -183,11 +183,7 @@ func Marshal(
183183
return ret, err
184184
}
185185

186-
func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, schemas *terraform.Schemas) error {
187-
if len(vars) == 0 {
188-
return nil
189-
}
190-
186+
func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, decls map[string]*configs.Variable) error {
191187
p.Variables = make(variables, len(vars))
192188

193189
for k, v := range vars {
@@ -203,6 +199,41 @@ func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, schemas
203199
Value: valJSON,
204200
}
205201
}
202+
203+
// In Terraform v1.1 and earlier we had some confusion about which subsystem
204+
// of Terraform was the one responsible for substituting in default values
205+
// for unset module variables, with root module variables being handled in
206+
// three different places while child module variables were only handled
207+
// during the Terraform Core graph walk.
208+
//
209+
// For Terraform v1.2 and later we rationalized that by having the Terraform
210+
// Core graph walk always be responsible for selecting defaults regardless
211+
// of root vs. child module, but unfortunately our earlier accidental
212+
// misbehavior bled out into the public interface by making the defaults
213+
// show up in the "vars" map to this function. Those are now correctly
214+
// omitted (so that the plan file only records the variables _actually_
215+
// set by the caller) but consumers of the JSON plan format may be depending
216+
// on our old behavior and so we'll fake it here just in time so that
217+
// outside consumers won't see a behavior change.
218+
for name, decl := range decls {
219+
if _, ok := p.Variables[name]; ok {
220+
continue
221+
}
222+
if val := decl.Default; val != cty.NilVal {
223+
valJSON, err := ctyjson.Marshal(val, val.Type())
224+
if err != nil {
225+
return err
226+
}
227+
p.Variables[name] = &variable{
228+
Value: valJSON,
229+
}
230+
}
231+
}
232+
233+
if len(p.Variables) == 0 {
234+
p.Variables = nil // omit this property if there are no variables to describe
235+
}
236+
206237
return nil
207238
}
208239

internal/terraform/context_apply2_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ resource "test_resource" "b" {
426426
},
427427
})
428428

429-
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
429+
plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
430430
assertNoErrors(t, diags)
431431

432432
_, diags = ctx.Apply(plan, m)
@@ -530,14 +530,14 @@ resource "test_object" "y" {
530530
},
531531
})
532532

533-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
533+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
534534
assertNoErrors(t, diags)
535535

536536
state, diags := ctx.Apply(plan, m)
537537
assertNoErrors(t, diags)
538538

539539
// FINAL PLAN:
540-
plan, diags = ctx.Plan(m, state, DefaultPlanOpts)
540+
plan, diags = ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
541541
assertNoErrors(t, diags)
542542

543543
// make sure the same marks are compared in the next plan as well

internal/terraform/context_apply_test.go

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ func TestContext2Apply_mapVarBetweenModules(t *testing.T) {
517517
},
518518
})
519519

520-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
520+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
521521
assertNoErrors(t, diags)
522522

523523
state, diags := ctx.Apply(plan, m)
@@ -2262,7 +2262,7 @@ func TestContext2Apply_countVariable(t *testing.T) {
22622262
},
22632263
})
22642264

2265-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
2265+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
22662266
assertNoErrors(t, diags)
22672267

22682268
state, diags := ctx.Apply(plan, m)
@@ -2288,7 +2288,7 @@ func TestContext2Apply_countVariableRef(t *testing.T) {
22882288
},
22892289
})
22902290

2291-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
2291+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
22922292
assertNoErrors(t, diags)
22932293

22942294
state, diags := ctx.Apply(plan, m)
@@ -2327,7 +2327,7 @@ func TestContext2Apply_provisionerInterpCount(t *testing.T) {
23272327
Provisioners: provisioners,
23282328
})
23292329

2330-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
2330+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
23312331
assertNoErrors(t, diags)
23322332

23332333
// We'll marshal and unmarshal the plan here, to ensure that we have
@@ -3682,7 +3682,7 @@ func TestContext2Apply_multiVarOrder(t *testing.T) {
36823682
},
36833683
})
36843684

3685-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
3685+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
36863686
assertNoErrors(t, diags)
36873687

36883688
state, diags := ctx.Apply(plan, m)
@@ -3713,7 +3713,7 @@ func TestContext2Apply_multiVarOrderInterp(t *testing.T) {
37133713
},
37143714
})
37153715

3716-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
3716+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
37173717
assertNoErrors(t, diags)
37183718

37193719
state, diags := ctx.Apply(plan, m)
@@ -4704,9 +4704,7 @@ func TestContext2Apply_provisionerDestroy(t *testing.T) {
47044704
},
47054705
})
47064706

4707-
plan, diags := ctx.Plan(m, state, &PlanOpts{
4708-
Mode: plans.DestroyMode,
4709-
})
4707+
plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.DestroyMode, testInputValuesUnset(m.Module.Variables)))
47104708
assertNoErrors(t, diags)
47114709

47124710
state, diags = ctx.Apply(plan, m)
@@ -4753,9 +4751,7 @@ func TestContext2Apply_provisionerDestroyFail(t *testing.T) {
47534751
},
47544752
})
47554753

4756-
plan, diags := ctx.Plan(m, state, &PlanOpts{
4757-
Mode: plans.DestroyMode,
4758-
})
4754+
plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.DestroyMode, testInputValuesUnset(m.Module.Variables)))
47594755
assertNoErrors(t, diags)
47604756

47614757
state, diags = ctx.Apply(plan, m)
@@ -5908,7 +5904,7 @@ func TestContext2Apply_destroyWithModuleVariableAndCountNested(t *testing.T) {
59085904
})
59095905

59105906
// First plan and apply a create operation
5911-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
5907+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
59125908
assertNoErrors(t, diags)
59135909

59145910
state, diags = ctx.Apply(plan, m)
@@ -5929,9 +5925,7 @@ func TestContext2Apply_destroyWithModuleVariableAndCountNested(t *testing.T) {
59295925
})
59305926

59315927
// First plan and apply a create operation
5932-
plan, diags := ctx.Plan(m, state, &PlanOpts{
5933-
Mode: plans.DestroyMode,
5934-
})
5928+
plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.DestroyMode, testInputValuesUnset(m.Module.Variables)))
59355929
if diags.HasErrors() {
59365930
t.Fatalf("destroy plan err: %s", diags.Err())
59375931
}
@@ -7561,6 +7555,12 @@ func TestContext2Apply_vars(t *testing.T) {
75617555
Value: cty.StringVal("us-east-1"),
75627556
SourceType: ValueFromCaller,
75637557
},
7558+
"bar": &InputValue{
7559+
// This one is not explicitly set but that's okay because it
7560+
// has a declared default, which Terraform Core will use instead.
7561+
Value: cty.NilVal,
7562+
SourceType: ValueFromCaller,
7563+
},
75647564
"test_list": &InputValue{
75657565
Value: cty.ListVal([]cty.Value{
75667566
cty.StringVal("Hello"),
@@ -7876,7 +7876,7 @@ func TestContext2Apply_issue7824(t *testing.T) {
78767876
},
78777877
})
78787878

7879-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
7879+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
78807880
if diags.HasErrors() {
78817881
t.Fatalf("err: %s", diags.Err())
78827882
}
@@ -7932,7 +7932,7 @@ func TestContext2Apply_issue5254(t *testing.T) {
79327932
},
79337933
})
79347934

7935-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
7935+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
79367936
if diags.HasErrors() {
79377937
t.Fatalf("err: %s", diags.Err())
79387938
}
@@ -7951,7 +7951,7 @@ func TestContext2Apply_issue5254(t *testing.T) {
79517951
},
79527952
})
79537953

7954-
plan, diags = ctx.Plan(m, state, DefaultPlanOpts)
7954+
plan, diags = ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
79557955
if diags.HasErrors() {
79567956
t.Fatalf("err: %s", diags.Err())
79577957
}
@@ -8845,7 +8845,7 @@ func TestContext2Apply_plannedInterpolatedCount(t *testing.T) {
88458845
Providers: Providers,
88468846
})
88478847

8848-
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
8848+
plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
88498849
if diags.HasErrors() {
88508850
t.Fatalf("plan failed: %s", diags.Err())
88518851
}
@@ -8904,9 +8904,7 @@ func TestContext2Apply_plannedDestroyInterpolatedCount(t *testing.T) {
89048904
Providers: providers,
89058905
})
89068906

8907-
plan, diags := ctx.Plan(m, state, &PlanOpts{
8908-
Mode: plans.DestroyMode,
8909-
})
8907+
plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.DestroyMode, testInputValuesUnset(m.Module.Variables)))
89108908
if diags.HasErrors() {
89118909
t.Fatalf("plan failed: %s", diags.Err())
89128910
}
@@ -9674,7 +9672,7 @@ func TestContext2Apply_plannedConnectionRefs(t *testing.T) {
96749672
Hooks: []Hook{hook},
96759673
})
96769674

9677-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
9675+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
96789676
diags.HasErrors()
96799677
if diags.HasErrors() {
96809678
t.Fatalf("diags: %s", diags.Err())
@@ -11687,7 +11685,7 @@ resource "test_resource" "foo" {
1168711685
},
1168811686
})
1168911687

11690-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
11688+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
1169111689
assertNoErrors(t, diags)
1169211690

1169311691
state, diags := ctx.Apply(plan, m)
@@ -11702,7 +11700,7 @@ resource "test_resource" "foo" {
1170211700
},
1170311701
})
1170411702

11705-
plan, diags = ctx.Plan(m, state, DefaultPlanOpts)
11703+
plan, diags = ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
1170611704
assertNoErrors(t, diags)
1170711705

1170811706
state, diags = ctx.Apply(plan, m)
@@ -11720,6 +11718,7 @@ resource "test_resource" "foo" {
1172011718
plan, diags = ctx.Plan(m, state, &PlanOpts{
1172111719
Mode: plans.NormalMode,
1172211720
SetVariables: InputValues{
11721+
"sensitive_id": &InputValue{Value: cty.NilVal},
1172311722
"sensitive_var": &InputValue{
1172411723
Value: cty.StringVal("bar"),
1172511724
},
@@ -11759,7 +11758,7 @@ resource "test_resource" "foo" {
1175911758
},
1176011759
})
1176111760

11762-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
11761+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
1176311762
if diags.HasErrors() {
1176411763
t.Fatalf("plan errors: %s", diags.Err())
1176511764
}
@@ -11904,7 +11903,7 @@ resource "test_resource" "foo" {
1190411903
)
1190511904
})
1190611905

11907-
plan, diags := ctx.Plan(m, state, DefaultPlanOpts)
11906+
plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
1190811907
assertNoErrors(t, diags)
1190911908

1191011909
addr := mustResourceInstanceAddr("test_resource.foo")
@@ -11954,7 +11953,7 @@ resource "test_resource" "foo" {
1195411953
// but this seems rather suspicious and we should ideally figure out what
1195511954
// this test was originally intending to do and make it do that.
1195611955
oldPlan := plan
11957-
_, diags = ctx2.Plan(m2, state, DefaultPlanOpts)
11956+
_, diags = ctx2.Plan(m2, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
1195811957
assertNoErrors(t, diags)
1195911958
stateWithoutSensitive, diags := ctx.Apply(oldPlan, m)
1196011959
assertNoErrors(t, diags)
@@ -12206,7 +12205,7 @@ func TestContext2Apply_dataSensitive(t *testing.T) {
1220612205
},
1220712206
})
1220812207

12209-
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
12208+
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
1221012209
if diags.HasErrors() {
1221112210
t.Fatalf("diags: %s", diags.Err())
1221212211
} else {

internal/terraform/context_eval.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func (c *Context) Eval(config *configs.Config, state *states.State, moduleAddr a
4545
state = state.DeepCopy()
4646
var walker *ContextGraphWalker
4747

48-
variables := mergeDefaultInputVariableValues(opts.SetVariables, config.Module.Variables)
48+
variables := opts.SetVariables
4949

5050
// By the time we get here, we should have values defined for all of
5151
// the root module variables, even if some of them are "unknown". It's the

internal/terraform/context_eval_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ func TestContextEval(t *testing.T) {
5454
},
5555
})
5656

57-
scope, diags := ctx.Eval(m, states.NewState(), addrs.RootModuleInstance, &EvalOpts{})
57+
scope, diags := ctx.Eval(m, states.NewState(), addrs.RootModuleInstance, &EvalOpts{
58+
SetVariables: testInputValuesUnset(m.Module.Variables),
59+
})
5860
if diags.HasErrors() {
5961
t.Fatalf("Eval errors: %s", diags.Err())
6062
}

internal/terraform/context_import.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (c *Context) Import(config *configs.Config, prevRunState *states.State, opt
5353

5454
log.Printf("[DEBUG] Building and walking import graph")
5555

56-
variables := mergeDefaultInputVariableValues(opts.SetVariables, config.Module.Variables)
56+
variables := opts.SetVariables
5757

5858
// Initialize our graph builder
5959
builder := &ImportGraphBuilder{

0 commit comments

Comments
 (0)