stackruntime: Apply defaults to root variables

When evaluating a stack's root input variables, supplied by the caller,
we must apply any default values specified in the variable
configuration for variables with no specified value. This commit adds
this default fallback case, using NilVal as a marker indicating the lack
of a specified value.

If no default value exists for a variable, it is therefore required to
be supplied by the caller. This commit also reports a diagnostic error
in this case.
pull/34765/head
Alisdair McDiarmid 2 years ago
parent 33bf5203b8
commit 0fe26468cd

@ -109,8 +109,34 @@ func (v *InputVariable) CheckValue(ctx context.Context, phase EvalPhase) (cty.Va
switch {
case v.Addr().Stack.IsRoot():
extVal := v.main.RootVariableValue(ctx, v.Addr().Item, phase)
wantTy := v.Declaration(ctx).Type.Constraint
extVal := v.main.RootVariableValue(ctx, v.Addr().Item, phase)
// If the calling context does not define a value for this
// variable, we need to fall back to the default.
if extVal.Value == cty.NilVal {
cfg := v.Config(ctx)
// A separate code path will validate the default value, so
// we don't need to do that here.
defVal := cfg.DefaultValue(ctx)
if defVal == cty.NilVal {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "No value for required variable",
Detail: fmt.Sprintf("The root input variable %q is not set, and has no default value.", v.Addr()),
Subject: cfg.config.DeclRange.ToHCL().Ptr(),
})
return cty.UnknownVal(wantTy), diags
}
extVal = ExternalInputValue{
Value: defVal,
DefRange: cfg.Declaration().DeclRange,
}
}
val, err := convert.Convert(extVal.Value, wantTy)
const errSummary = "Invalid value for root input variable"
if err != nil {

@ -442,6 +442,10 @@ func (m *Main) PreviousProviderInstances(addr stackaddrs.AbsComponentInstance, p
}
}
// RootVariableValue returns the original root variable value specified by the
// caller, if any. The caller of this function is responsible for replacing
// missing values with defaults, and performing type conversion and and
// validation.
func (m *Main) RootVariableValue(ctx context.Context, addr stackaddrs.InputVariable, phase EvalPhase) ExternalInputValue {
switch phase {
case PlanPhase:
@ -450,8 +454,12 @@ func (m *Main) RootVariableValue(ctx context.Context, addr stackaddrs.InputVaria
}
ret, ok := m.planning.opts.InputVariableValues[addr]
if !ok {
// If no value is specified for the given input variable, we return
// a nil placeholder. Nil can never be specified, so the caller can
// determine that the variable's default value should be used (if
// present) or an error raised (if not).
return ExternalInputValue{
Value: cty.NullVal(cty.DynamicPseudoType),
Value: cty.NilVal,
}
}
return ret

@ -234,6 +234,57 @@ func TestPlanWithMissingInputVariable(t *testing.T) {
}
}
func TestPlanWithNoValueForRequiredVariable(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "plan-no-value-for-required-variable")
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
if err != nil {
t.Fatal(err)
}
changesCh := make(chan stackplan.PlannedChange, 8)
diagsCh := make(chan tfdiags.Diagnostic, 2)
req := PlanRequest{
Config: cfg,
ProviderFactories: map[addrs.Provider]providers.Factory{
addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
return terraformProvider.NewProvider(), nil
},
},
ForcePlanTimestamp: &fakePlanTimestamp,
}
resp := PlanResponse{
PlannedChanges: changesCh,
Diagnostics: diagsCh,
}
go Plan(ctx, &req, &resp)
_, gotDiags := collectPlanOutput(changesCh, diagsCh)
// We'll normalize the diagnostics to be of consistent underlying type
// using ForRPC, so that we can easily diff them; we don't actually care
// about which underlying implementation is in use.
gotDiags = gotDiags.ForRPC()
var wantDiags tfdiags.Diagnostics
wantDiags = wantDiags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "No value for required variable",
Detail: `The root input variable "var.beep" is not set, and has no default value.`,
Subject: &hcl.Range{
Filename: mainBundleSourceAddrStr("plan-no-value-for-required-variable/unset-variable.tfstack.hcl"),
Start: hcl.Pos{Line: 1, Column: 1, Byte: 0},
End: hcl.Pos{Line: 1, Column: 16, Byte: 15},
},
})
wantDiags = wantDiags.ForRPC()
if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
t.Errorf("wrong diagnostics\n%s", diff)
}
}
func TestPlanWithSingleResource(t *testing.T) {
ctx := context.Background()
cfg := loadMainBundleConfigForTest(t, "with-single-resource")
@ -433,7 +484,7 @@ func TestPlanVariableOutputRoundtripNested(t *testing.T) {
Addr: stackaddrs.InputVariable{
Name: "msg",
},
Value: cty.NullVal(cty.String),
Value: cty.StringVal("default"),
},
}
sort.SliceStable(gotChanges, func(i, j int) bool {

@ -50,8 +50,16 @@ var (
}),
},
},
filepath.Join("with-single-input", "provider-name-clash"): {},
filepath.Join("with-single-input", "valid"): {},
filepath.Join("with-single-input", "provider-name-clash"): {
planInputVars: map[string]cty.Value{
"input": cty.StringVal("input"),
},
},
filepath.Join("with-single-input", "valid"): {
planInputVars: map[string]cty.Value{
"input": cty.StringVal("input"),
},
},
}
// invalidConfigurations are shared between the validate and plan tests.

Loading…
Cancel
Save