mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-28 18:01:01 -06:00
36c4d4c241
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.)
97 lines
3.7 KiB
Go
97 lines
3.7 KiB
Go
package terraform
|
|
|
|
import (
|
|
"log"
|
|
|
|
"github.com/hashicorp/terraform/internal/addrs"
|
|
"github.com/hashicorp/terraform/internal/configs"
|
|
"github.com/hashicorp/terraform/internal/lang"
|
|
"github.com/hashicorp/terraform/internal/states"
|
|
"github.com/hashicorp/terraform/internal/tfdiags"
|
|
)
|
|
|
|
type EvalOpts struct {
|
|
SetVariables InputValues
|
|
}
|
|
|
|
// Eval produces a scope in which expressions can be evaluated for
|
|
// the given module path.
|
|
//
|
|
// This method must first evaluate any ephemeral values (input variables, local
|
|
// values, and output values) in the configuration. These ephemeral values are
|
|
// not included in the persisted state, so they must be re-computed using other
|
|
// values in the state before they can be properly evaluated. The updated
|
|
// values are retained in the main state associated with the receiving context.
|
|
//
|
|
// This function takes no action against remote APIs but it does need access
|
|
// to all provider and provisioner instances in order to obtain their schemas
|
|
// for type checking.
|
|
//
|
|
// The result is an evaluation scope that can be used to resolve references
|
|
// against the root module. If the returned diagnostics contains errors then
|
|
// the returned scope may be nil. If it is not nil then it may still be used
|
|
// to attempt expression evaluation or other analysis, but some expressions
|
|
// may not behave as expected.
|
|
func (c *Context) Eval(config *configs.Config, state *states.State, moduleAddr addrs.ModuleInstance, opts *EvalOpts) (*lang.Scope, tfdiags.Diagnostics) {
|
|
// This is intended for external callers such as the "terraform console"
|
|
// command. Internally, we create an evaluator in c.walk before walking
|
|
// the graph, and create scopes in ContextGraphWalker.
|
|
|
|
var diags tfdiags.Diagnostics
|
|
defer c.acquireRun("eval")()
|
|
|
|
// Start with a copy of state so that we don't affect the instance that
|
|
// the caller is holding.
|
|
state = state.DeepCopy()
|
|
var walker *ContextGraphWalker
|
|
|
|
variables := opts.SetVariables
|
|
|
|
// By the time we get here, we should have values defined for all of
|
|
// the root module variables, even if some of them are "unknown". It's the
|
|
// caller's responsibility to have already handled the decoding of these
|
|
// from the various ways the CLI allows them to be set and to produce
|
|
// user-friendly error messages if they are not all present, and so
|
|
// the error message from checkInputVariables should never be seen and
|
|
// includes language asking the user to report a bug.
|
|
varDiags := checkInputVariables(config.Module.Variables, variables)
|
|
diags = diags.Append(varDiags)
|
|
|
|
log.Printf("[DEBUG] Building and walking 'eval' graph")
|
|
|
|
graph, moreDiags := (&EvalGraphBuilder{
|
|
Config: config,
|
|
State: state,
|
|
RootVariableValues: variables,
|
|
Plugins: c.plugins,
|
|
}).Build(addrs.RootModuleInstance)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
return nil, diags
|
|
}
|
|
|
|
walkOpts := &graphWalkOpts{
|
|
InputState: state,
|
|
Config: config,
|
|
}
|
|
|
|
walker, moreDiags = c.walk(graph, walkEval, walkOpts)
|
|
diags = diags.Append(moreDiags)
|
|
if walker != nil {
|
|
diags = diags.Append(walker.NonFatalDiagnostics)
|
|
} else {
|
|
// If we skipped walking the graph (due to errors) then we'll just
|
|
// use a placeholder graph walker here, which'll refer to the
|
|
// unmodified state.
|
|
walker = c.graphWalker(walkEval, walkOpts)
|
|
}
|
|
|
|
// This is a bit weird since we don't normally evaluate outside of
|
|
// the context of a walk, but we'll "re-enter" our desired path here
|
|
// just to get hold of an EvalContext for it. ContextGraphWalker
|
|
// caches its contexts, so we should get hold of the context that was
|
|
// previously used for evaluation here, unless we skipped walking.
|
|
evalCtx := walker.EnterPath(moduleAddr)
|
|
return evalCtx.EvaluationScope(nil, EvalDataForNoInstanceKey), diags
|
|
}
|