terraform/internal/moduletest/graph/variables.go
2026-02-17 13:56:34 +00:00

216 lines
7.8 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package graph
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/lang/langrefs"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// GetVariables builds the terraform.InputValues required for the provided run
// block. It pulls the relevant variables (ie. the variables needed for the
// run block) from the total pool of all available variables, and converts them
// into input values.
//
// As a run block can reference variables defined within the file and are not
// actually defined within the configuration, this function actually returns
// more variables than are required by the config. FilterVariablesToConfig
// should be called before trying to use these variables within a Terraform
// plan, apply, or destroy operation.
func GetVariables(ctx *EvalContext, run *configs.TestRun, module *configs.Config, includeWarnings bool) (terraform.InputValues, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// relevantVariables contains the variables that are of interest to this
// run block. This is a combination of the variables declared within the
// configuration for this run block, and the variables referenced by the
// run block assertions.
relevantVariables := make(map[string]*addrs.Reference)
// First, we'll check to see which variables the run block assertions
// reference.
references, _ := moduletest.GetRunReferences(run)
for _, reference := range references {
if addr, ok := reference.Subject.(addrs.InputVariable); ok {
relevantVariables[addr.Name] = reference
}
}
// And check to see which variables the run block configuration references.
for name := range module.Module.Variables {
relevantVariables[name] = nil
}
// We'll put the parsed values into this map.
values := make(terraform.InputValues)
// First, let's step through the expressions within the run block and work
// them out.
for name, expr := range run.Variables {
refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, expr)
diags = append(diags, refDiags...)
if refDiags.HasErrors() {
continue
}
ctx, ctxDiags := ctx.HclContext(refs)
diags = diags.Append(ctxDiags)
value := cty.DynamicVal
if !ctxDiags.HasErrors() {
var valueDiags hcl.Diagnostics
value, valueDiags = expr.Value(ctx)
diags = diags.Append(valueDiags)
}
// We do this late on so we still validate whatever it was that the user
// wrote in the variable expression. But, we don't want to actually use
// it if it's not actually relevant.
if _, exists := relevantVariables[name]; !exists {
// Do not display warnings during cleanup phase
if includeWarnings {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Value for undeclared variable",
Detail: fmt.Sprintf("The module under test does not declare a variable named %q, but it is declared in run block %q.", name, run.Name),
Subject: expr.Range().Ptr(),
})
}
continue // Don't add it to our final set of variables.
}
values[name] = &terraform.InputValue{
Value: value,
SourceType: terraform.ValueFromConfig,
SourceRange: tfdiags.SourceRangeFromHCL(expr.Range()),
}
}
// Second, let's see if we have any variables defined with the configuration
// we're about to test. We'll check if we have matching variable values
// defined within the test file or globally that can match them and, if not,
// use a default fallback value to let Terraform attempt to apply defaults
// if they exist.
for name, variable := range module.Module.Variables {
if _, exists := values[name]; exists {
// Then we've provided a variable for this explicitly. It's all
// good.
continue
}
// The user might have provided a value for this externally or at the
// file level, so we can also just pass it through.
if value, ok := ctx.GetVariable(variable.Name); ok {
values[name] = value
continue
}
if value, valueDiags := ctx.EvaluateUnparsedVariable(name, variable); value != nil {
diags = diags.Append(valueDiags)
values[name] = value
continue
}
// If all else fails, these variables may have default values set within
// the to-be-executed Terraform config. We'll put in placeholder values
// if that is the case, otherwise add a diagnostic early to avoid
// executing something we know will fail.
if variable.Required() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "No value for required variable",
Detail: fmt.Sprintf("The module under test for run block %q has a required variable %q with no set value. Use a -var or -var-file command line argument or add this variable into a \"variables\" block within the test file or run block.",
run.Name, variable.Name),
Subject: variable.DeclRange.Ptr(),
})
values[name] = &terraform.InputValue{
Value: cty.DynamicVal,
SourceType: terraform.ValueFromConfig,
SourceRange: tfdiags.SourceRangeFromHCL(variable.DeclRange),
}
} else {
values[name] = &terraform.InputValue{
Value: cty.NilVal,
SourceType: terraform.ValueFromConfig,
SourceRange: tfdiags.SourceRangeFromHCL(variable.DeclRange),
}
}
}
// Finally, in this whole thing we might have other variables that are
// just referenced by parts of this run block. These must have been defined
// elsewhere, but we need to include them.
for variable, reference := range relevantVariables {
if _, exists := values[variable]; exists {
// Then we've already got a value for this variable.
continue
}
// Otherwise, we'll get it from the cache as a file-level or global
// variable.
if value, ok := ctx.GetVariable(variable); ok {
values[variable] = value
continue
}
if reference == nil {
// this shouldn't happen, we only put nil references into the
// relevantVariables map for values derived from the configuration
// and all of these should have been set in previous for loop.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing reference",
Detail: fmt.Sprintf("The variable %q had no point of reference, which should not be possible. This is a bug in Terraform; please report it!", variable),
})
continue
}
if value, valueDiags := ctx.EvaluateUnparsedVariableDeprecated(variable, reference); value != nil {
values[variable] = value
diags = diags.Append(valueDiags)
}
}
return values, diags
}
// FilterVariablesToModule splits the provided values into two disjoint maps:
// moduleVars contains the ones that correspond with declarations in the root
// module of the given configuration, while testOnlyVars contains any others
// that are presumably intended only for use in the test configuration file.
//
// This function is essentially the opposite of AddVariablesToConfig which
// makes the config match the variables rather than the variables match the
// config.
//
// This function can only return warnings, and the callers can rely on this so
// please check the callers of this function if you add any error diagnostics.
func FilterVariablesToModule(config *configs.Config, values terraform.InputValues) (moduleVars, testOnlyVars terraform.InputValues, diags tfdiags.Diagnostics) {
moduleVars = make(terraform.InputValues)
testOnlyVars = make(terraform.InputValues)
for name, value := range values {
_, exists := config.Module.Variables[name]
if !exists {
// If it's not in the configuration then it's a test-only variable.
testOnlyVars[name] = value
continue
}
moduleVars[name] = value
}
return moduleVars, testOnlyVars, diags
}