core: Handle root and child module input variables consistently

Previously we had a significant discrepancy between these two situations:
we wrote the raw root module variables directly into the EvalContext and
then applied type conversions only at expression evaluation time, while
for child modules we converted and validated the values while visiting
the variable graph node and wrote only the _final_ value into the
EvalContext.

This confusion seems to have been the root cause for #29899, where
validation rules for root module variables were being applied at the wrong
point in the process, prior to type conversion.

To fix that bug and also make similar mistakes less likely in the future,
I've made the root module variable handling more like the child module
variable handling in the following ways:
 - The "raw value" (exactly as given by the user) lives only in the graph
   node representing the variable, which mirrors how the _expression_
   for a child module variable lives in its graph node. This means that
   the flow for the two is the same except that there's no expression
   evaluation step for root module variables, because they arrive as
   constant values from the caller.
 - The set of variable values in the EvalContext is always only "final"
   values, after type conversion is complete. That in turn means we no
   longer need to do "just in time" conversion in
   evaluationStateData.GetInputVariable, and can just return the value
   exactly as stored, which is consistent with how we handle all other
   references between objects.

This diff is noisier than I'd like because of how much it takes to wire
a new argument (the raw variable values) through to the plan graph builder,
but those changes are pretty mechanical and the interesting logic lives
inside the plan graph builder itself, in NodeRootVariable, and
the shared helper functions in eval_variable.go.

While here I also took the opportunity to fix a historical API wart in
EvalContext, where SetModuleCallArguments was built to take a set of
variable values all at once but our current caller always calls with only
one at a time. That is now just SetModuleCallArgument singular, to match
with the new SetRootModuleArgument to deal with root module variables.
This commit is contained in:
Martin Atkins 2021-11-10 17:29:45 -08:00
parent 483c38aca1
commit 37b1413ab3
21 changed files with 1012 additions and 257 deletions

View File

@ -30,30 +30,11 @@ func (c *Context) Apply(plan *plans.Plan, config *configs.Config) (*states.State
return nil, diags return nil, diags
} }
variables := InputValues{}
for name, dyVal := range plan.VariableValues {
val, err := dyVal.Decode(cty.DynamicPseudoType)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid variable value in plan",
fmt.Sprintf("Invalid value for variable %q recorded in plan file: %s.", name, err),
))
continue
}
variables[name] = &InputValue{
Value: val,
SourceType: ValueFromPlan,
}
}
workingState := plan.PriorState.DeepCopy() workingState := plan.PriorState.DeepCopy()
walker, walkDiags := c.walk(graph, operation, &graphWalkOpts{ walker, walkDiags := c.walk(graph, operation, &graphWalkOpts{
Config: config, Config: config,
InputState: workingState, InputState: workingState,
Changes: plan.Changes, Changes: plan.Changes,
RootVariableValues: variables,
}) })
diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walker.NonFatalDiagnostics)
diags = diags.Append(walkDiags) diags = diags.Append(walkDiags)
@ -83,15 +64,43 @@ Note that the -target option is not suitable for routine use, and is provided on
} }
func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate bool) (*Graph, walkOperation, tfdiags.Diagnostics) { func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate bool) (*Graph, walkOperation, tfdiags.Diagnostics) {
graph, diags := (&ApplyGraphBuilder{ var diags tfdiags.Diagnostics
variables := InputValues{}
for name, dyVal := range plan.VariableValues {
val, err := dyVal.Decode(cty.DynamicPseudoType)
if err != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Invalid variable value in plan",
fmt.Sprintf("Invalid value for variable %q recorded in plan file: %s.", name, err),
))
continue
}
variables[name] = &InputValue{
Value: val,
SourceType: ValueFromPlan,
}
}
if diags.HasErrors() {
return nil, walkApply, diags
}
graph, moreDiags := (&ApplyGraphBuilder{
Config: config, Config: config,
Changes: plan.Changes, Changes: plan.Changes,
State: plan.PriorState, State: plan.PriorState,
RootVariableValues: variables,
Plugins: c.plugins, Plugins: c.plugins,
Targets: plan.TargetAddrs, Targets: plan.TargetAddrs,
ForceReplace: plan.ForceReplaceAddrs, ForceReplace: plan.ForceReplaceAddrs,
Validate: validate, Validate: validate,
}).Build(addrs.RootModuleInstance) }).Build(addrs.RootModuleInstance)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return nil, walkApply, diags
}
operation := walkApply operation := walkApply
if plan.UIMode == plans.DestroyMode { if plan.UIMode == plans.DestroyMode {

View File

@ -62,6 +62,7 @@ func (c *Context) Eval(config *configs.Config, state *states.State, moduleAddr a
graph, moreDiags := (&EvalGraphBuilder{ graph, moreDiags := (&EvalGraphBuilder{
Config: config, Config: config,
State: state, State: state,
RootVariableValues: variables,
Plugins: c.plugins, Plugins: c.plugins,
}).Build(addrs.RootModuleInstance) }).Build(addrs.RootModuleInstance)
diags = diags.Append(moreDiags) diags = diags.Append(moreDiags)
@ -72,7 +73,6 @@ func (c *Context) Eval(config *configs.Config, state *states.State, moduleAddr a
walkOpts := &graphWalkOpts{ walkOpts := &graphWalkOpts{
InputState: state, InputState: state,
Config: config, Config: config,
RootVariableValues: variables,
} }
walker, moreDiags = c.walk(graph, walkEval, walkOpts) walker, moreDiags = c.walk(graph, walkEval, walkOpts)

View File

@ -53,10 +53,13 @@ func (c *Context) Import(config *configs.Config, prevRunState *states.State, opt
log.Printf("[DEBUG] Building and walking import graph") log.Printf("[DEBUG] Building and walking import graph")
variables := mergeDefaultInputVariableValues(opts.SetVariables, config.Module.Variables)
// Initialize our graph builder // Initialize our graph builder
builder := &ImportGraphBuilder{ builder := &ImportGraphBuilder{
ImportTargets: opts.Targets, ImportTargets: opts.Targets,
Config: config, Config: config,
RootVariableValues: variables,
Plugins: c.plugins, Plugins: c.plugins,
} }
@ -67,13 +70,10 @@ func (c *Context) Import(config *configs.Config, prevRunState *states.State, opt
return state, diags return state, diags
} }
variables := mergeDefaultInputVariableValues(opts.SetVariables, config.Module.Variables)
// Walk it // Walk it
walker, walkDiags := c.walk(graph, walkImport, &graphWalkOpts{ walker, walkDiags := c.walk(graph, walkImport, &graphWalkOpts{
Config: config, Config: config,
InputState: state, InputState: state,
RootVariableValues: variables,
}) })
diags = diags.Append(walkDiags) diags = diags.Append(walkDiags)
if walkDiags.HasErrors() { if walkDiags.HasErrors() {

View File

@ -125,11 +125,11 @@ The -target option is not for routine use, and is provided only for exceptional
var planDiags tfdiags.Diagnostics var planDiags tfdiags.Diagnostics
switch opts.Mode { switch opts.Mode {
case plans.NormalMode: case plans.NormalMode:
plan, planDiags = c.plan(config, prevRunState, variables, opts) plan, planDiags = c.plan(config, prevRunState, opts)
case plans.DestroyMode: case plans.DestroyMode:
plan, planDiags = c.destroyPlan(config, prevRunState, variables, opts) plan, planDiags = c.destroyPlan(config, prevRunState, opts)
case plans.RefreshOnlyMode: case plans.RefreshOnlyMode:
plan, planDiags = c.refreshOnlyPlan(config, prevRunState, variables, opts) plan, planDiags = c.refreshOnlyPlan(config, prevRunState, opts)
default: default:
panic(fmt.Sprintf("unsupported plan mode %s", opts.Mode)) panic(fmt.Sprintf("unsupported plan mode %s", opts.Mode))
} }
@ -172,14 +172,14 @@ var DefaultPlanOpts = &PlanOpts{
Mode: plans.NormalMode, Mode: plans.NormalMode,
} }
func (c *Context) plan(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { func (c *Context) plan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
if opts.Mode != plans.NormalMode { if opts.Mode != plans.NormalMode {
panic(fmt.Sprintf("called Context.plan with %s", opts.Mode)) panic(fmt.Sprintf("called Context.plan with %s", opts.Mode))
} }
plan, walkDiags := c.planWalk(config, prevRunState, rootVariables, opts) plan, walkDiags := c.planWalk(config, prevRunState, opts)
diags = diags.Append(walkDiags) diags = diags.Append(walkDiags)
if diags.HasErrors() { if diags.HasErrors() {
return nil, diags return nil, diags
@ -194,14 +194,14 @@ func (c *Context) plan(config *configs.Config, prevRunState *states.State, rootV
return plan, diags return plan, diags
} }
func (c *Context) refreshOnlyPlan(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { func (c *Context) refreshOnlyPlan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
if opts.Mode != plans.RefreshOnlyMode { if opts.Mode != plans.RefreshOnlyMode {
panic(fmt.Sprintf("called Context.refreshOnlyPlan with %s", opts.Mode)) panic(fmt.Sprintf("called Context.refreshOnlyPlan with %s", opts.Mode))
} }
plan, walkDiags := c.planWalk(config, prevRunState, rootVariables, opts) plan, walkDiags := c.planWalk(config, prevRunState, opts)
diags = diags.Append(walkDiags) diags = diags.Append(walkDiags)
if diags.HasErrors() { if diags.HasErrors() {
return nil, diags return nil, diags
@ -235,7 +235,7 @@ func (c *Context) refreshOnlyPlan(config *configs.Config, prevRunState *states.S
return plan, diags return plan, diags
} }
func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
pendingPlan := &plans.Plan{} pendingPlan := &plans.Plan{}
@ -260,7 +260,7 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State
log.Printf("[TRACE] Context.destroyPlan: calling Context.plan to get the effect of refreshing the prior state") log.Printf("[TRACE] Context.destroyPlan: calling Context.plan to get the effect of refreshing the prior state")
normalOpts := *opts normalOpts := *opts
normalOpts.Mode = plans.NormalMode normalOpts.Mode = plans.NormalMode
refreshPlan, refreshDiags := c.plan(config, prevRunState, rootVariables, &normalOpts) refreshPlan, refreshDiags := c.plan(config, prevRunState, &normalOpts)
if refreshDiags.HasErrors() { if refreshDiags.HasErrors() {
// NOTE: Normally we'd append diagnostics regardless of whether // NOTE: Normally we'd append diagnostics regardless of whether
// there are errors, just in case there are warnings we'd want to // there are errors, just in case there are warnings we'd want to
@ -291,7 +291,7 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State
priorState = pendingPlan.PriorState priorState = pendingPlan.PriorState
} }
destroyPlan, walkDiags := c.planWalk(config, priorState, rootVariables, opts) destroyPlan, walkDiags := c.planWalk(config, priorState, opts)
diags = diags.Append(walkDiags) diags = diags.Append(walkDiags)
if walkDiags.HasErrors() { if walkDiags.HasErrors() {
return nil, diags return nil, diags
@ -392,7 +392,7 @@ func (c *Context) postPlanValidateMoves(config *configs.Config, stmts []refactor
return refactoring.ValidateMoves(stmts, config, allInsts) return refactoring.ValidateMoves(stmts, config, allInsts)
} }
func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, rootVariables InputValues, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
log.Printf("[DEBUG] Building and walking plan graph for %s", opts.Mode) log.Printf("[DEBUG] Building and walking plan graph for %s", opts.Mode)
@ -423,7 +423,6 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, r
InputState: prevRunState, InputState: prevRunState,
Changes: changes, Changes: changes,
MoveResults: moveResults, MoveResults: moveResults,
RootVariableValues: rootVariables,
}) })
diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walker.NonFatalDiagnostics)
diags = diags.Append(walkDiags) diags = diags.Append(walkDiags)
@ -471,6 +470,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
graph, diags := (&PlanGraphBuilder{ graph, diags := (&PlanGraphBuilder{
Config: config, Config: config,
State: prevRunState, State: prevRunState,
RootVariableValues: opts.SetVariables,
Plugins: c.plugins, Plugins: c.plugins,
Targets: opts.Targets, Targets: opts.Targets,
ForceReplace: opts.ForceReplace, ForceReplace: opts.ForceReplace,
@ -482,6 +482,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
graph, diags := (&PlanGraphBuilder{ graph, diags := (&PlanGraphBuilder{
Config: config, Config: config,
State: prevRunState, State: prevRunState,
RootVariableValues: opts.SetVariables,
Plugins: c.plugins, Plugins: c.plugins,
Targets: opts.Targets, Targets: opts.Targets,
Validate: validate, Validate: validate,
@ -493,6 +494,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
graph, diags := (&DestroyPlanGraphBuilder{ graph, diags := (&DestroyPlanGraphBuilder{
Config: config, Config: config,
State: prevRunState, State: prevRunState,
RootVariableValues: opts.SetVariables,
Plugins: c.plugins, Plugins: c.plugins,
Targets: opts.Targets, Targets: opts.Targets,
Validate: validate, Validate: validate,

View File

@ -37,17 +37,6 @@ func (c *Context) Validate(config *configs.Config) tfdiags.Diagnostics {
log.Printf("[DEBUG] Building and walking validate graph") log.Printf("[DEBUG] Building and walking validate graph")
graph, moreDiags := ValidateGraphBuilder(&PlanGraphBuilder{
Config: config,
Plugins: c.plugins,
Validate: true,
State: states.NewState(),
}).Build(addrs.RootModuleInstance)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return diags
}
// Validate is to check if the given module is valid regardless of // Validate is to check if the given module is valid regardless of
// input values, current state, etc. Therefore we populate all of the // input values, current state, etc. Therefore we populate all of the
// input values with unknown values of the expected type, allowing us // input values with unknown values of the expected type, allowing us
@ -66,9 +55,20 @@ func (c *Context) Validate(config *configs.Config) tfdiags.Diagnostics {
} }
} }
graph, moreDiags := ValidateGraphBuilder(&PlanGraphBuilder{
Config: config,
Plugins: c.plugins,
Validate: true,
State: states.NewState(),
RootVariableValues: varValues,
}).Build(addrs.RootModuleInstance)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return diags
}
walker, walkDiags := c.walk(graph, walkValidate, &graphWalkOpts{ walker, walkDiags := c.walk(graph, walkValidate, &graphWalkOpts{
Config: config, Config: config,
RootVariableValues: varValues,
}) })
diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walker.NonFatalDiagnostics)
diags = diags.Append(walkDiags) diags = diags.Append(walkDiags)

View File

@ -23,7 +23,6 @@ type graphWalkOpts struct {
Changes *plans.Changes Changes *plans.Changes
Config *configs.Config Config *configs.Config
RootVariableValues InputValues
MoveResults refactoring.MoveResults MoveResults refactoring.MoveResults
} }
@ -108,6 +107,5 @@ func (c *Context) graphWalker(operation walkOperation, opts *graphWalkOpts) *Con
MoveResults: opts.MoveResults, MoveResults: opts.MoveResults,
Operation: operation, Operation: operation,
StopContext: c.runContext, StopContext: c.runContext,
RootVariableValues: opts.RootVariableValues,
} }
} }

View File

@ -121,12 +121,24 @@ type EvalContext interface {
// addresses in this context. // addresses in this context.
EvaluationScope(self addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope EvaluationScope(self addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope
// SetModuleCallArguments defines values for the variables of a particular // SetRootModuleArgument defines the value for one variable of the root
// child module call. // module. The caller must ensure that given value is a suitable
// "final value" for the variable, which means that it's already converted
// and validated to match any configured constraints and validation rules.
// //
// Calling this function multiple times has merging behavior, keeping any // Calling this function multiple times with the same variable address
// previously-set keys that are not present in the new map. // will silently overwrite the value provided by a previous call.
SetModuleCallArguments(addrs.ModuleCallInstance, map[string]cty.Value) SetRootModuleArgument(addrs.InputVariable, cty.Value)
// SetModuleCallArgument defines the value for one input variable of a
// particular child module call. The caller must ensure that the given
// value is a suitable "final value" for the variable, which means that
// it's already converted and validated to match any configured
// constraints and validation rules.
//
// Calling this function multiple times with the same variable address
// will silently overwrite the value provided by a previous call.
SetModuleCallArgument(addrs.ModuleCallInstance, addrs.InputVariable, cty.Value)
// GetVariableValue returns the value provided for the input variable with // GetVariableValue returns the value provided for the input variable with
// the given address, or cty.DynamicVal if the variable hasn't been assigned // the given address, or cty.DynamicVal if the variable hasn't been assigned

View File

@ -313,7 +313,21 @@ func (ctx *BuiltinEvalContext) Path() addrs.ModuleInstance {
return ctx.PathValue return ctx.PathValue
} }
func (ctx *BuiltinEvalContext) SetModuleCallArguments(n addrs.ModuleCallInstance, vals map[string]cty.Value) { func (ctx *BuiltinEvalContext) SetRootModuleArgument(addr addrs.InputVariable, v cty.Value) {
ctx.VariableValuesLock.Lock()
defer ctx.VariableValuesLock.Unlock()
log.Printf("[TRACE] BuiltinEvalContext: Storing final value for variable %s", addr.Absolute(addrs.RootModuleInstance))
key := addrs.RootModuleInstance.String()
args := ctx.VariableValues[key]
if args == nil {
args = make(map[string]cty.Value)
ctx.VariableValues[key] = args
}
args[addr.Name] = v
}
func (ctx *BuiltinEvalContext) SetModuleCallArgument(callAddr addrs.ModuleCallInstance, varAddr addrs.InputVariable, v cty.Value) {
ctx.VariableValuesLock.Lock() ctx.VariableValuesLock.Lock()
defer ctx.VariableValuesLock.Unlock() defer ctx.VariableValuesLock.Unlock()
@ -321,18 +335,15 @@ func (ctx *BuiltinEvalContext) SetModuleCallArguments(n addrs.ModuleCallInstance
panic("context path not set") panic("context path not set")
} }
childPath := n.ModuleInstance(ctx.PathValue) childPath := callAddr.ModuleInstance(ctx.PathValue)
log.Printf("[TRACE] BuiltinEvalContext: Storing final value for variable %s", varAddr.Absolute(childPath))
key := childPath.String() key := childPath.String()
args := ctx.VariableValues[key] args := ctx.VariableValues[key]
if args == nil { if args == nil {
ctx.VariableValues[key] = vals args = make(map[string]cty.Value)
return ctx.VariableValues[key] = args
}
for k, v := range vals {
args[k] = v
} }
args[varAddr.Name] = v
} }
func (ctx *BuiltinEvalContext) GetVariableValue(addr addrs.AbsInputVariableInstance) cty.Value { func (ctx *BuiltinEvalContext) GetVariableValue(addr addrs.AbsInputVariableInstance) cty.Value {

View File

@ -111,13 +111,21 @@ type MockEvalContext struct {
PathCalled bool PathCalled bool
PathPath addrs.ModuleInstance PathPath addrs.ModuleInstance
SetModuleCallArgumentsCalled bool SetRootModuleArgumentCalled bool
SetModuleCallArgumentsModule addrs.ModuleCallInstance SetRootModuleArgumentAddr addrs.InputVariable
SetModuleCallArgumentsValues map[string]cty.Value SetRootModuleArgumentValue cty.Value
SetRootModuleArgumentFunc func(addr addrs.InputVariable, v cty.Value)
SetModuleCallArgumentCalled bool
SetModuleCallArgumentModuleCall addrs.ModuleCallInstance
SetModuleCallArgumentVariable addrs.InputVariable
SetModuleCallArgumentValue cty.Value
SetModuleCallArgumentFunc func(callAddr addrs.ModuleCallInstance, varAddr addrs.InputVariable, v cty.Value)
GetVariableValueCalled bool GetVariableValueCalled bool
GetVariableValueAddr addrs.AbsInputVariableInstance GetVariableValueAddr addrs.AbsInputVariableInstance
GetVariableValueValue cty.Value GetVariableValueValue cty.Value
GetVariableValueFunc func(addr addrs.AbsInputVariableInstance) cty.Value // supersedes GetVariableValueValue
ChangesCalled bool ChangesCalled bool
ChangesChanges *plans.ChangesSync ChangesChanges *plans.ChangesSync
@ -321,15 +329,31 @@ func (c *MockEvalContext) Path() addrs.ModuleInstance {
return c.PathPath return c.PathPath
} }
func (c *MockEvalContext) SetModuleCallArguments(n addrs.ModuleCallInstance, values map[string]cty.Value) { func (c *MockEvalContext) SetRootModuleArgument(addr addrs.InputVariable, v cty.Value) {
c.SetModuleCallArgumentsCalled = true c.SetRootModuleArgumentCalled = true
c.SetModuleCallArgumentsModule = n c.SetRootModuleArgumentAddr = addr
c.SetModuleCallArgumentsValues = values c.SetRootModuleArgumentValue = v
if c.SetRootModuleArgumentFunc != nil {
c.SetRootModuleArgumentFunc(addr, v)
}
}
func (c *MockEvalContext) SetModuleCallArgument(callAddr addrs.ModuleCallInstance, varAddr addrs.InputVariable, v cty.Value) {
c.SetModuleCallArgumentCalled = true
c.SetModuleCallArgumentModuleCall = callAddr
c.SetModuleCallArgumentVariable = varAddr
c.SetModuleCallArgumentValue = v
if c.SetModuleCallArgumentFunc != nil {
c.SetModuleCallArgumentFunc(callAddr, varAddr, v)
}
} }
func (c *MockEvalContext) GetVariableValue(addr addrs.AbsInputVariableInstance) cty.Value { func (c *MockEvalContext) GetVariableValue(addr addrs.AbsInputVariableInstance) cty.Value {
c.GetVariableValueCalled = true c.GetVariableValueCalled = true
c.GetVariableValueAddr = addr c.GetVariableValueAddr = addr
if c.GetVariableValueFunc != nil {
return c.GetVariableValueFunc(addr)
}
return c.GetVariableValueValue return c.GetVariableValueValue
} }

View File

@ -12,6 +12,102 @@ import (
"github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/convert"
) )
func prepareFinalInputVariableValue(addr addrs.AbsInputVariableInstance, given cty.Value, valRange tfdiags.SourceRange, cfg *configs.Variable) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
convertTy := cfg.ConstraintType
log.Printf("[TRACE] prepareFinalInputVariableValue: preparing %s", addr)
var defaultVal cty.Value
if cfg.Default != cty.NilVal {
log.Printf("[TRACE] prepareFinalInputVariableValue: %s has a default value", addr)
var err error
defaultVal, err = convert.Convert(cfg.Default, convertTy)
if err != nil {
// Validation of the declaration should typically catch this,
// but we'll check it here too to be robust.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid default value for module argument",
Detail: fmt.Sprintf(
"The default value for variable %q is incompatible with its type constraint: %s.",
cfg.Name, err,
),
Subject: &cfg.DeclRange,
})
// We'll return a placeholder unknown value to avoid producing
// redundant downstream errors.
return cty.UnknownVal(cfg.Type), diags
}
}
if given == cty.NilVal { // The variable wasn't set at all (even to null)
log.Printf("[TRACE] prepareFinalInputVariableValue: %s has no defined value", addr)
if cfg.Required() {
// NOTE: The CLI layer typically checks for itself whether all of
// the required _root_ module variables are not set, which would
// mask this error. We can get here for child module variables,
// though.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Required variable not set`,
Detail: fmt.Sprintf(`The variable %q is required, but is not set.`, addr.Variable.Name),
Subject: valRange.ToHCL().Ptr(),
})
// We'll return a placeholder unknown value to avoid producing
// redundant downstream errors.
return cty.UnknownVal(cfg.Type), diags
}
given = defaultVal // must be set, because we checked above that the variable isn't required
}
val, err := convert.Convert(given, convertTy)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid value for module argument",
Detail: fmt.Sprintf(
"The given value is not suitable for child module variable %q defined at %s: %s.",
cfg.Name, cfg.DeclRange.String(), err,
),
Subject: valRange.ToHCL().Ptr(),
})
// We'll return a placeholder unknown value to avoid producing
// redundant downstream errors.
return cty.UnknownVal(cfg.Type), diags
}
// By the time we get here, we know:
// - val matches the variable's type constraint
// - val is definitely not cty.NilVal, but might be a null value if the given was already null.
//
// That means we just need to handle the case where the value is null,
// which might mean we need to use the default value, or produce an error.
//
// For historical reasons we do this only for a "non-nullable" variable.
// Nullable variables just appear as null if they were set to null,
// regardless of any default value.
if val.IsNull() && !cfg.Nullable {
log.Printf("[TRACE] prepareFinalInputVariableValue: %s is defined as null", addr)
if defaultVal != cty.NilVal {
val = defaultVal
} else {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Required variable not set`,
Detail: fmt.Sprintf(`The variable %q is required, but the given value is null.`, addr.Variable.Name),
Subject: valRange.ToHCL().Ptr(),
})
// Stub out our return value so that the semantic checker doesn't
// produce redundant downstream errors.
val = cty.UnknownVal(cfg.Type)
}
}
return val, diags
}
// evalVariableValidations ensures that all of the configured custom validations // evalVariableValidations ensures that all of the configured custom validations
// for a variable are passing. // for a variable are passing.
// //
@ -20,9 +116,10 @@ import (
// EvalModuleCallArgument for variables in descendent modules. // EvalModuleCallArgument for variables in descendent modules.
func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *configs.Variable, expr hcl.Expression, ctx EvalContext) (diags tfdiags.Diagnostics) { func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *configs.Variable, expr hcl.Expression, ctx EvalContext) (diags tfdiags.Diagnostics) {
if config == nil || len(config.Validations) == 0 { if config == nil || len(config.Validations) == 0 {
log.Printf("[TRACE] evalVariableValidations: not active for %s, so skipping", addr) log.Printf("[TRACE] evalVariableValidations: no validation rules declared for %s, so skipping", addr)
return nil return nil
} }
log.Printf("[TRACE] evalVariableValidations: validating %s", addr)
// Variable nodes evaluate in the parent module to where they were declared // Variable nodes evaluate in the parent module to where they were declared
// because the value expression (n.Expr, if set) comes from the calling // because the value expression (n.Expr, if set) comes from the calling
@ -34,6 +131,14 @@ func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *config
// evaluation context containing just the required value, and thus avoid // evaluation context containing just the required value, and thus avoid
// the problem that ctx's evaluation functions refer to the wrong module. // the problem that ctx's evaluation functions refer to the wrong module.
val := ctx.GetVariableValue(addr) val := ctx.GetVariableValue(addr)
if val == cty.NilVal {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "No final value for variable",
Detail: fmt.Sprintf("Terraform doesn't have a final value for %s during validation. This is a bug in Terraform; please report it!", addr),
})
return diags
}
hclCtx := &hcl.EvalContext{ hclCtx := &hcl.EvalContext{
Variables: map[string]cty.Value{ Variables: map[string]cty.Value{
"var": cty.ObjectVal(map[string]cty.Value{ "var": cty.ObjectVal(map[string]cty.Value{

View File

@ -0,0 +1,426 @@
package terraform
import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/tfdiags"
)
func TestPrepareFinalInputVariableValue(t *testing.T) {
// This is just a concise way to define a bunch of *configs.Variable
// objects to use in our tests below. We're only going to decode this
// config, not fully evaluate it.
cfgSrc := `
variable "nullable_required" {
}
variable "nullable_optional_default_string" {
default = "hello"
}
variable "nullable_optional_default_null" {
default = null
}
variable "constrained_string_nullable_required" {
type = string
}
variable "constrained_string_nullable_optional_default_string" {
type = string
default = "hello"
}
variable "constrained_string_nullable_optional_default_bool" {
type = string
default = true
}
variable "constrained_string_nullable_optional_default_null" {
type = string
default = null
}
variable "required" {
nullable = false
}
variable "optional_default_string" {
nullable = false
default = "hello"
}
variable "constrained_string_required" {
nullable = false
type = string
}
variable "constrained_string_optional_default_string" {
nullable = false
type = string
default = "hello"
}
variable "constrained_string_optional_default_bool" {
nullable = false
type = string
default = true
}
`
cfg := testModuleInline(t, map[string]string{
"main.tf": cfgSrc,
})
variableConfigs := cfg.Module.Variables
tests := []struct {
varName string
given cty.Value
want cty.Value
wantErr string
}{
// nullable_required
{
"nullable_required",
cty.NilVal,
cty.UnknownVal(cty.DynamicPseudoType),
`Required variable not set: The variable "nullable_required" is required, but is not set.`,
},
{
"nullable_required",
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType),
``, // "required" for a nullable variable means only that it must be set, even if it's set to null
},
{
"nullable_required",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"nullable_required",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
// nullable_optional_default_string
{
"nullable_optional_default_string",
cty.NilVal,
cty.StringVal("hello"), // the declared default value
``,
},
{
"nullable_optional_default_string",
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.DynamicPseudoType), // nullable variables can be really set to null, masking the default
``,
},
{
"nullable_optional_default_string",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"nullable_optional_default_string",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
// nullable_optional_default_null
{
"nullable_optional_default_null",
cty.NilVal,
cty.NullVal(cty.DynamicPseudoType), // the declared default value
``,
},
{
"nullable_optional_default_null",
cty.NullVal(cty.String),
cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default
``,
},
{
"nullable_optional_default_null",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"nullable_optional_default_null",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
// constrained_string_nullable_required
{
"constrained_string_nullable_required",
cty.NilVal,
cty.UnknownVal(cty.String),
`Required variable not set: The variable "constrained_string_nullable_required" is required, but is not set.`,
},
{
"constrained_string_nullable_required",
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.String), // the null value still gets converted to match the type constraint
``, // "required" for a nullable variable means only that it must be set, even if it's set to null
},
{
"constrained_string_nullable_required",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"constrained_string_nullable_required",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
// constrained_string_nullable_optional_default_string
{
"constrained_string_nullable_optional_default_string",
cty.NilVal,
cty.StringVal("hello"), // the declared default value
``,
},
{
"constrained_string_nullable_optional_default_string",
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default
``,
},
{
"constrained_string_nullable_optional_default_string",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"constrained_string_nullable_optional_default_string",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
// constrained_string_nullable_optional_default_bool
{
"constrained_string_nullable_optional_default_bool",
cty.NilVal,
cty.StringVal("true"), // the declared default value, automatically converted to match type constraint
``,
},
{
"constrained_string_nullable_optional_default_bool",
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.String), // nullable variables can be really set to null, masking the default
``,
},
{
"constrained_string_nullable_optional_default_bool",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"constrained_string_nullable_optional_default_bool",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
// constrained_string_nullable_optional_default_null
{
"constrained_string_nullable_optional_default_null",
cty.NilVal,
cty.NullVal(cty.String),
``,
},
{
"constrained_string_nullable_optional_default_null",
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.String),
``,
},
{
"constrained_string_nullable_optional_default_null",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"constrained_string_nullable_optional_default_null",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
// required
{
"required",
cty.NilVal,
cty.UnknownVal(cty.DynamicPseudoType),
`Required variable not set: The variable "required" is required, but is not set.`,
},
{
"required",
cty.NullVal(cty.DynamicPseudoType),
cty.UnknownVal(cty.DynamicPseudoType),
`Required variable not set: The variable "required" is required, but the given value is null.`,
},
{
"required",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"required",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
// optional_default_string
{
"optional_default_string",
cty.NilVal,
cty.StringVal("hello"), // the declared default value
``,
},
{
"optional_default_string",
cty.NullVal(cty.DynamicPseudoType),
cty.StringVal("hello"), // the declared default value
``,
},
{
"optional_default_string",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"optional_default_string",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
// constrained_string_required
{
"constrained_string_required",
cty.NilVal,
cty.UnknownVal(cty.String),
`Required variable not set: The variable "constrained_string_required" is required, but is not set.`,
},
{
"constrained_string_required",
cty.NullVal(cty.DynamicPseudoType),
cty.UnknownVal(cty.String),
`Required variable not set: The variable "constrained_string_required" is required, but the given value is null.`,
},
{
"constrained_string_required",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"constrained_string_required",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
// constrained_string_optional_default_string
{
"constrained_string_optional_default_string",
cty.NilVal,
cty.StringVal("hello"), // the declared default value
``,
},
{
"constrained_string_optional_default_string",
cty.NullVal(cty.DynamicPseudoType),
cty.StringVal("hello"), // the declared default value
``,
},
{
"constrained_string_optional_default_string",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"constrained_string_optional_default_string",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
// constrained_string_optional_default_bool
{
"constrained_string_optional_default_bool",
cty.NilVal,
cty.StringVal("true"), // the declared default value, automatically converted to match type constraint
``,
},
{
"constrained_string_optional_default_bool",
cty.NullVal(cty.DynamicPseudoType),
cty.StringVal("true"), // the declared default value, automatically converted to match type constraint
``,
},
{
"constrained_string_optional_default_bool",
cty.StringVal("ahoy"),
cty.StringVal("ahoy"),
``,
},
{
"constrained_string_optional_default_bool",
cty.UnknownVal(cty.String),
cty.UnknownVal(cty.String),
``,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s %#v", test.varName, test.given), func(t *testing.T) {
varAddr := addrs.InputVariable{Name: test.varName}.Absolute(addrs.RootModuleInstance)
varCfg := variableConfigs[test.varName]
if varCfg == nil {
t.Fatalf("invalid variable name %q", test.varName)
}
t.Logf(
"test case\nvariable: %s\nconstraint: %#v\ndefault: %#v\nnullable: %#v\ngiven value: %#v",
varAddr,
varCfg.Type,
varCfg.Default,
varCfg.Nullable,
test.given,
)
got, diags := prepareFinalInputVariableValue(
varAddr, test.given, tfdiags.SourceRangeFromHCL(varCfg.DeclRange), varCfg,
)
if test.wantErr != "" {
if !diags.HasErrors() {
t.Errorf("unexpected success\nwant error: %s", test.wantErr)
} else if got, want := diags.Err().Error(), test.wantErr; got != want {
t.Errorf("wrong error\ngot: %s\nwant: %s", got, want)
}
} else {
if diags.HasErrors() {
t.Errorf("unexpected error\ngot: %s", diags.Err().Error())
}
}
// NOTE: should still have returned some reasonable value even if there was an error
if !test.want.RawEquals(got) {
t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.want)
}
})
}
}

View File

@ -10,7 +10,6 @@ import (
"github.com/agext/levenshtein" "github.com/agext/levenshtein"
"github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs"
@ -248,7 +247,7 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
// This is important because otherwise the validation walk will tend to be // This is important because otherwise the validation walk will tend to be
// overly strict, requiring expressions throughout the configuration to // overly strict, requiring expressions throughout the configuration to
// be complicated to accommodate all possible inputs, whereas returning // be complicated to accommodate all possible inputs, whereas returning
// known here allows for simpler patterns like using input values as // unknown here allows for simpler patterns like using input values as
// guards to broadly enable/disable resources, avoid processing things // guards to broadly enable/disable resources, avoid processing things
// that are disabled, etc. Terraform's static validation leans towards // that are disabled, etc. Terraform's static validation leans towards
// being liberal in what it accepts because the subsequent plan walk has // being liberal in what it accepts because the subsequent plan walk has
@ -267,28 +266,27 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd
return cty.UnknownVal(config.Type), diags return cty.UnknownVal(config.Type), diags
} }
// d.Evaluator.VariableValues should always contain valid "final values"
// for variables, which is to say that they have already had type
// conversions, validations, and default value handling applied to them.
// Those are the responsibility of the graph notes representing the
// variable declarations. Therefore here we just trust that we already
// have a correct value.
val, isSet := vals[addr.Name] val, isSet := vals[addr.Name]
switch { if !isSet {
case !isSet: // We should not be able to get here without having a valid value
// The config loader will ensure there is a default if the value is not // for every variable, so this always indicates a bug in either
// set at all. // the graph builder (not including all the needed nodes) or in
val = config.Default // the graph nodes representing variables.
case val.IsNull() && !config.Nullable && config.Default != cty.NilVal:
// If nullable=false a null value will use the configured default.
val = config.Default
}
var err error
val, err = convert.Convert(val, config.ConstraintType)
if err != nil {
// We should never get here because this problem should've been caught
// during earlier validation, but we'll do something reasonable anyway.
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: `Incorrect variable type`, Summary: `Reference to unresolved input variable`,
Detail: fmt.Sprintf(`The resolved value of variable %q is not appropriate: %s.`, addr.Name, err), Detail: fmt.Sprintf(
Subject: &config.DeclRange, `The final value for %s is missing in Terraform's evaluation context. This is a bug in Terraform; please report it!`,
addr.Absolute(d.ModulePath),
),
Subject: rng.ToHCL().Ptr(),
}) })
val = cty.UnknownVal(config.Type) val = cty.UnknownVal(config.Type)
} }

View File

@ -26,6 +26,11 @@ type ApplyGraphBuilder struct {
// State is the current state // State is the current state
State *states.State State *states.State
// RootVariableValues are the root module input variables captured as
// part of the plan object, which we must reproduce in the apply step
// to get a consistent result.
RootVariableValues InputValues
// Plugins is a library of the plug-in components (providers and // Plugins is a library of the plug-in components (providers and
// provisioners) available for use. // provisioners) available for use.
Plugins *contextPlugins Plugins *contextPlugins
@ -88,7 +93,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
}, },
// Add dynamic values // Add dynamic values
&RootVariableTransformer{Config: b.Config}, &RootVariableTransformer{Config: b.Config, RawValues: b.RootVariableValues},
&ModuleVariableTransformer{Config: b.Config}, &ModuleVariableTransformer{Config: b.Config},
&LocalTransformer{Config: b.Config}, &LocalTransformer{Config: b.Config},
&OutputTransformer{Config: b.Config, Changes: b.Changes}, &OutputTransformer{Config: b.Config, Changes: b.Changes},

View File

@ -23,6 +23,11 @@ type DestroyPlanGraphBuilder struct {
// State is the current state // State is the current state
State *states.State State *states.State
// RootVariableValues are the raw input values for root input variables
// given by the caller, which we'll resolve into final values as part
// of the plan walk.
RootVariableValues InputValues
// Plugins is a library of plug-in components (providers and // Plugins is a library of plug-in components (providers and
// provisioners) available for use. // provisioners) available for use.
Plugins *contextPlugins Plugins *contextPlugins

View File

@ -30,6 +30,11 @@ type EvalGraphBuilder struct {
// State is the current state // State is the current state
State *states.State State *states.State
// RootVariableValues are the raw input values for root input variables
// given by the caller, which we'll resolve into final values as part
// of the plan walk.
RootVariableValues InputValues
// Plugins is a library of plug-in components (providers and // Plugins is a library of plug-in components (providers and
// provisioners) available for use. // provisioners) available for use.
Plugins *contextPlugins Plugins *contextPlugins
@ -60,7 +65,7 @@ func (b *EvalGraphBuilder) Steps() []GraphTransformer {
}, },
// Add dynamic values // Add dynamic values
&RootVariableTransformer{Config: b.Config}, &RootVariableTransformer{Config: b.Config, RawValues: b.RootVariableValues},
&ModuleVariableTransformer{Config: b.Config}, &ModuleVariableTransformer{Config: b.Config},
&LocalTransformer{Config: b.Config}, &LocalTransformer{Config: b.Config},
&OutputTransformer{Config: b.Config}, &OutputTransformer{Config: b.Config},

View File

@ -17,6 +17,11 @@ type ImportGraphBuilder struct {
// Module is a configuration to build the graph from. See ImportOpts.Config. // Module is a configuration to build the graph from. See ImportOpts.Config.
Config *configs.Config Config *configs.Config
// RootVariableValues are the raw input values for root input variables
// given by the caller, which we'll resolve into final values as part
// of the plan walk.
RootVariableValues InputValues
// Plugins is a library of plug-in components (providers and // Plugins is a library of plug-in components (providers and
// provisioners) available for use. // provisioners) available for use.
Plugins *contextPlugins Plugins *contextPlugins
@ -53,7 +58,7 @@ func (b *ImportGraphBuilder) Steps() []GraphTransformer {
&ConfigTransformer{Config: config}, &ConfigTransformer{Config: config},
// Add dynamic values // Add dynamic values
&RootVariableTransformer{Config: b.Config}, &RootVariableTransformer{Config: b.Config, RawValues: b.RootVariableValues},
&ModuleVariableTransformer{Config: b.Config}, &ModuleVariableTransformer{Config: b.Config},
&LocalTransformer{Config: b.Config}, &LocalTransformer{Config: b.Config},
&OutputTransformer{Config: b.Config}, &OutputTransformer{Config: b.Config},

View File

@ -28,6 +28,11 @@ type PlanGraphBuilder struct {
// State is the current state // State is the current state
State *states.State State *states.State
// RootVariableValues are the raw input values for root input variables
// given by the caller, which we'll resolve into final values as part
// of the plan walk.
RootVariableValues InputValues
// Plugins is a library of plug-in components (providers and // Plugins is a library of plug-in components (providers and
// provisioners) available for use. // provisioners) available for use.
Plugins *contextPlugins Plugins *contextPlugins
@ -95,7 +100,7 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer {
}, },
// Add dynamic values // Add dynamic values
&RootVariableTransformer{Config: b.Config}, &RootVariableTransformer{Config: b.Config, RawValues: b.RootVariableValues},
&ModuleVariableTransformer{Config: b.Config}, &ModuleVariableTransformer{Config: b.Config},
&LocalTransformer{Config: b.Config}, &LocalTransformer{Config: b.Config},
&OutputTransformer{Config: b.Config}, &OutputTransformer{Config: b.Config},

View File

@ -12,7 +12,6 @@ import (
"github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
) )
// nodeExpandModuleVariable is the placeholder for an variable that has not yet had // nodeExpandModuleVariable is the placeholder for an variable that has not yet had
@ -143,35 +142,27 @@ func (n *nodeModuleVariable) ModulePath() addrs.Module {
// GraphNodeExecutable // GraphNodeExecutable
func (n *nodeModuleVariable) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { func (n *nodeModuleVariable) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
// If we have no value, do nothing log.Printf("[TRACE] nodeModuleVariable: evaluating %s", n.Addr)
if n.Expr == nil {
return nil
}
// Otherwise, interpolate the value of this variable and set it var val cty.Value
// within the variables mapping.
var vals map[string]cty.Value
var err error var err error
switch op { switch op {
case walkValidate: case walkValidate:
vals, err = n.evalModuleCallArgument(ctx, true) val, err = n.evalModuleCallArgument(ctx, true)
diags = diags.Append(err) diags = diags.Append(err)
if diags.HasErrors() {
return diags
}
default: default:
vals, err = n.evalModuleCallArgument(ctx, false) val, err = n.evalModuleCallArgument(ctx, false)
diags = diags.Append(err) diags = diags.Append(err)
}
if diags.HasErrors() { if diags.HasErrors() {
return diags return diags
} }
}
// Set values for arguments of a child module call, for later retrieval // Set values for arguments of a child module call, for later retrieval
// during expression evaluation. // during expression evaluation.
_, call := n.Addr.Module.CallInstance() _, call := n.Addr.Module.CallInstance()
ctx.SetModuleCallArguments(call, vals) ctx.SetModuleCallArgument(call, n.Addr.Variable, val)
return evalVariableValidations(n.Addr, n.Config, n.Expr, ctx) return evalVariableValidations(n.Addr, n.Config, n.Expr, ctx)
} }
@ -199,19 +190,11 @@ func (n *nodeModuleVariable) DotNode(name string, opts *dag.DotOpts) *dag.DotNod
// validateOnly indicates that this evaluation is only for config // validateOnly indicates that this evaluation is only for config
// validation, and we will not have any expansion module instance // validation, and we will not have any expansion module instance
// repetition data. // repetition data.
func (n *nodeModuleVariable) evalModuleCallArgument(ctx EvalContext, validateOnly bool) (map[string]cty.Value, error) { func (n *nodeModuleVariable) evalModuleCallArgument(ctx EvalContext, validateOnly bool) (cty.Value, error) {
name := n.Addr.Variable.Name var diags tfdiags.Diagnostics
expr := n.Expr var givenVal cty.Value
var errSourceRange tfdiags.SourceRange
if expr == nil { if expr := n.Expr; expr != nil {
// Should never happen, but we'll bail out early here rather than
// crash in case it does. We set no value at all in this case,
// making a subsequent call to EvalContext.SetModuleCallArguments
// a no-op.
log.Printf("[ERROR] attempt to evaluate %s with nil expression", n.Addr.String())
return nil, nil
}
var moduleInstanceRepetitionData instances.RepetitionData var moduleInstanceRepetitionData instances.RepetitionData
switch { switch {
@ -231,45 +214,21 @@ func (n *nodeModuleVariable) evalModuleCallArgument(ctx EvalContext, validateOnl
} }
scope := ctx.EvaluationScope(nil, moduleInstanceRepetitionData) scope := ctx.EvaluationScope(nil, moduleInstanceRepetitionData)
val, diags := scope.EvalExpr(expr, cty.DynamicPseudoType) val, moreDiags := scope.EvalExpr(expr, cty.DynamicPseudoType)
diags = diags.Append(moreDiags)
// We intentionally passed DynamicPseudoType to EvalExpr above because if moreDiags.HasErrors() {
// now we can do our own local type conversion and produce an error message return cty.DynamicVal, diags.ErrWithWarnings()
// with better context if it fails. }
var convErr error givenVal = val
val, convErr = convert.Convert(val, n.Config.ConstraintType) errSourceRange = tfdiags.SourceRangeFromHCL(expr.Range())
if convErr != nil { } else {
diags = diags.Append(&hcl.Diagnostic{ // We'll use cty.NilVal to represent the variable not being set at all.
Severity: hcl.DiagError, givenVal = cty.NilVal
Summary: "Invalid value for module argument", errSourceRange = tfdiags.SourceRangeFromHCL(n.Config.DeclRange) // we use the declaration range as a fallback for an undefined variable
Detail: fmt.Sprintf(
"The given value is not suitable for child module variable %q defined at %s: %s.",
name, n.Config.DeclRange.String(), convErr,
),
Subject: expr.Range().Ptr(),
})
// We'll return a placeholder unknown value to avoid producing
// redundant downstream errors.
val = cty.UnknownVal(n.Config.Type)
} }
// If there is no default, we have to ensure that a null value is allowed finalVal, moreDiags := prepareFinalInputVariableValue(n.Addr, givenVal, errSourceRange, n.Config)
// for this variable. diags = diags.Append(moreDiags)
if n.Config.Default == cty.NilVal && !n.Config.Nullable && val.IsNull() {
// The value cannot be null, and there is no configured default.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid variable value`,
Detail: fmt.Sprintf(`The variable %q is required, but the given value is null.`, n.Addr),
Subject: &n.Config.DeclRange,
})
// Stub out our return value so that the semantic checker doesn't
// produce redundant downstream errors.
val = cty.UnknownVal(n.Config.Type)
}
vals := make(map[string]cty.Value) return finalVal, diags.ErrWithWarnings()
vals[name] = val
return vals, diags.ErrWithWarnings()
} }

View File

@ -1,16 +1,26 @@
package terraform package terraform
import ( import (
"log"
"github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
) )
// NodeRootVariable represents a root variable input. // NodeRootVariable represents a root variable input.
type NodeRootVariable struct { type NodeRootVariable struct {
Addr addrs.InputVariable Addr addrs.InputVariable
Config *configs.Variable Config *configs.Variable
// RawValue is the value for the variable set from outside Terraform
// Core, such as on the command line, or from an environment variable,
// or similar. This is the raw value that was provided, not yet
// converted or validated, and can be nil for a variable that isn't
// set at all.
RawValue *InputValue
} }
var ( var (
@ -38,21 +48,56 @@ func (n *NodeRootVariable) ReferenceableAddrs() []addrs.Referenceable {
// GraphNodeExecutable // GraphNodeExecutable
func (n *NodeRootVariable) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { func (n *NodeRootVariable) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics {
// We don't actually need to _evaluate_ a root module variable, because // Root module variables are special in that they are provided directly
// its value is always constant and already stashed away in our EvalContext. // by the caller (usually, the CLI layer) and so we don't really need to
// However, we might need to run some user-defined validation rules against // evaluate them in the usual sense, but we do need to process the raw
// the value. // values given by the caller to match what the module is expecting, and
// make sure the values are valid.
var diags tfdiags.Diagnostics
if n.Config == nil || len(n.Config.Validations) == 0 { addr := addrs.RootModuleInstance.InputVariable(n.Addr.Name)
return nil // nothing to do log.Printf("[TRACE] NodeRootVariable: evaluating %s", addr)
if n.Config == nil {
// Because we build NodeRootVariable from configuration in the normal
// case it's strange to get here, but we tolerate it to allow for
// tests that might not populate the inputs fully.
return nil
} }
return evalVariableValidations( var givenVal cty.Value
if n.RawValue != nil {
givenVal = n.RawValue.Value
} else {
// We'll use cty.NilVal to represent the variable not being set at
// all, which for historical reasons is unfortunately different than
// explicitly setting it to null in some cases.
givenVal = cty.NilVal
}
finalVal, moreDiags := prepareFinalInputVariableValue(
addr,
givenVal,
tfdiags.SourceRangeFromHCL(n.Config.DeclRange),
n.Config,
)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
// No point in proceeding to validations then, because they'll
// probably fail trying to work with a value of the wrong type.
return diags
}
ctx.SetRootModuleArgument(addr.Variable, finalVal)
moreDiags = evalVariableValidations(
addrs.RootModuleInstance.InputVariable(n.Addr.Name), addrs.RootModuleInstance.InputVariable(n.Addr.Name),
n.Config, n.Config,
nil, // not set for root module variables nil, // not set for root module variables
ctx, ctx,
) )
diags = diags.Append(moreDiags)
return diags
} }
// dag.GraphNodeDotter impl. // dag.GraphNodeDotter impl.

View File

@ -3,12 +3,16 @@ package terraform
import ( import (
"testing" "testing"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs"
"github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/lang"
) )
func TestNodeRootVariableExecute(t *testing.T) { func TestNodeRootVariableExecute(t *testing.T) {
t.Run("type conversion", func(t *testing.T) {
ctx := new(MockEvalContext) ctx := new(MockEvalContext)
n := &NodeRootVariable{ n := &NodeRootVariable{
@ -18,6 +22,10 @@ func TestNodeRootVariableExecute(t *testing.T) {
Type: cty.String, Type: cty.String,
ConstraintType: cty.String, ConstraintType: cty.String,
}, },
RawValue: &InputValue{
Value: cty.True,
SourceType: ValueFromUnknown,
},
} }
diags := n.Execute(ctx, walkApply) diags := n.Execute(ctx, walkApply)
@ -25,4 +33,134 @@ func TestNodeRootVariableExecute(t *testing.T) {
t.Fatalf("unexpected error: %s", diags.Err()) t.Fatalf("unexpected error: %s", diags.Err())
} }
if !ctx.SetRootModuleArgumentCalled {
t.Fatalf("ctx.SetRootModuleArgument wasn't called")
}
if got, want := ctx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want {
t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want)
}
if got, want := ctx.SetRootModuleArgumentValue, cty.StringVal("true"); !want.RawEquals(got) {
// NOTE: The given value was cty.Bool but the type constraint was
// cty.String, so it was NodeRootVariable's responsibility to convert
// as part of preparing the "final value".
t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want)
}
})
t.Run("validation", func(t *testing.T) {
ctx := new(MockEvalContext)
// The variable validation function gets called with Terraform's
// built-in functions available, so we need a minimal scope just for
// it to get the functions from.
ctx.EvaluationScopeScope = &lang.Scope{}
// We need to reimplement a _little_ bit of EvalContextBuiltin logic
// here to get a similar effect with EvalContextMock just to get the
// value to flow through here in a realistic way that'll make this test
// useful.
var finalVal cty.Value
ctx.SetRootModuleArgumentFunc = func(addr addrs.InputVariable, v cty.Value) {
if addr.Name == "foo" {
t.Logf("set %s to %#v", addr.String(), v)
finalVal = v
}
}
ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value {
if addr.String() != "var.foo" {
return cty.NilVal
}
t.Logf("reading final val for %s (%#v)", addr.String(), finalVal)
return finalVal
}
n := &NodeRootVariable{
Addr: addrs.InputVariable{Name: "foo"},
Config: &configs.Variable{
Name: "foo",
Type: cty.Number,
ConstraintType: cty.Number,
Validations: []*configs.VariableValidation{
{
Condition: fakeHCLExpressionFunc(func(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
// This returns true only if the given variable value
// is exactly cty.Number, which allows us to verify
// that we were given the value _after_ type
// conversion.
// This had previously not been handled correctly,
// as reported in:
// https://github.com/hashicorp/terraform/issues/29899
vars := ctx.Variables["var"]
if vars == cty.NilVal || !vars.Type().IsObjectType() || !vars.Type().HasAttribute("foo") {
t.Logf("var.foo isn't available")
return cty.False, nil
}
val := vars.GetAttr("foo")
if val == cty.NilVal || val.Type() != cty.Number {
t.Logf("var.foo is %#v; want a number", val)
return cty.False, nil
}
return cty.True, nil
}),
ErrorMessage: "Must be a number.",
},
},
},
RawValue: &InputValue{
// Note: This is a string, but the variable's type constraint
// is number so it should be converted before use.
Value: cty.StringVal("5"),
SourceType: ValueFromUnknown,
},
}
diags := n.Execute(ctx, walkApply)
if diags.HasErrors() {
t.Fatalf("unexpected error: %s", diags.Err())
}
if !ctx.SetRootModuleArgumentCalled {
t.Fatalf("ctx.SetRootModuleArgument wasn't called")
}
if got, want := ctx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want {
t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want)
}
if got, want := ctx.SetRootModuleArgumentValue, cty.NumberIntVal(5); !want.RawEquals(got) {
// NOTE: The given value was cty.Bool but the type constraint was
// cty.String, so it was NodeRootVariable's responsibility to convert
// as part of preparing the "final value".
t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want)
}
})
}
// fakeHCLExpressionFunc is a fake implementation of hcl.Expression that just
// directly produces a value with direct Go code.
//
// An expression of this type has no references and so it cannot access any
// variables from the EvalContext unless something else arranges for them
// to be guaranteed available. For example, custom variable validations just
// unconditionally have access to the variable they are validating regardless
// of references.
type fakeHCLExpressionFunc func(*hcl.EvalContext) (cty.Value, hcl.Diagnostics)
var _ hcl.Expression = fakeHCLExpressionFunc(nil)
func (f fakeHCLExpressionFunc) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
return f(ctx)
}
func (f fakeHCLExpressionFunc) Variables() []hcl.Traversal {
return nil
}
func (f fakeHCLExpressionFunc) Range() hcl.Range {
return hcl.Range{
Filename: "fake",
Start: hcl.InitialPos,
End: hcl.InitialPos,
}
}
func (f fakeHCLExpressionFunc) StartRange() hcl.Range {
return f.Range()
} }

View File

@ -13,6 +13,8 @@ import (
// reach them. // reach them.
type RootVariableTransformer struct { type RootVariableTransformer struct {
Config *configs.Config Config *configs.Config
RawValues InputValues
} }
func (t *RootVariableTransformer) Transform(g *Graph) error { func (t *RootVariableTransformer) Transform(g *Graph) error {
@ -32,6 +34,7 @@ func (t *RootVariableTransformer) Transform(g *Graph) error {
Name: v.Name, Name: v.Name,
}, },
Config: v, Config: v,
RawValue: t.RawValues[v.Name],
} }
g.Add(node) g.Add(node)
} }