mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 18:10:30 -04:00
216 lines
7.8 KiB
Go
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
|
|
}
|