@ -4,6 +4,7 @@
package hcl
import (
"fmt"
"sync"
"github.com/hashicorp/hcl/v2"
@ -17,125 +18,118 @@ import (
"github.com/hashicorp/terraform/internal/tfdiags"
)
// VariableCaches contains a mapping between test run blocks and evaluated
// variables. This is used to cache the results of evaluating variables so that
// they are only evaluated once per run.
//
// Each run block has its own configuration and therefore its own set of
// evaluated variables.
type VariableCaches struct {
GlobalVariables map [ string ] backendrun . UnparsedVariableValue
FileVariables map [ string ] hcl . Expression
caches map [ string ] * VariableCache
cacheLock sync . Mutex
type VariableCache struct {
mutex sync . Mutex
// ExternalVariableValues contains the raw values provided by the user
// via either the CLI, environment variables, or a variable file.
ExternalVariableValues map [ string ] backendrun . UnparsedVariableValue
// TestFileVariableDefinitions contains the definitions for variables
// defined within the test file in `variable` blocks.
TestFileVariableDefinitions map [ string ] * configs . Variable
// TestFileVariableExpressions contains the concrete variable expressions
// defined within the test file `variables` block.
TestFileVariableExpressions map [ string ] hcl . Expression
// fileVariableValues contains the set of available file level
fileVariableValues map [ string ] * terraform . InputValue
}
func NewVariableCaches ( opts ... func ( * VariableCaches ) ) * VariableCaches {
ret := & VariableCaches {
GlobalVariables : make ( map [ string ] backendrun . UnparsedVariableValue ) ,
FileVariables : make ( map [ string ] hcl . Expression ) ,
caches : make ( map [ string ] * VariableCache ) ,
cacheLock : sync . Mutex { } ,
func ( cache * VariableCache ) EvaluateExternalVariable ( name string , config * configs . Variable ) ( * terraform . InputValue , tfdiags . Diagnostics ) {
variable , exists := cache . ExternalVariableValues [ name ]
if ! exists {
return nil , nil
}
for _ , opt := range opts {
opt ( ret )
if config != nil {
// If we have a configuration, then we'll using the parsing mode from
// that.
value , diags := variable . ParseVariableValue ( config . ParsingMode )
if diags . HasErrors ( ) {
value = & terraform . InputValue {
Value : cty . DynamicVal ,
}
}
return value , diags
}
return ret
}
// For backwards-compatibility reasons we do also have to support trying
// to parse the global variables without a configuration. We introduced the
// file-level variable definitions later, and users were already using
// global variables so we do need to keep supporting this use case.
// VariableCache contains the cache for a single run block. This cache contains
// the evaluated values for global and file-level variables.
type VariableCache struct {
config * configs . Config
// Otherwise, we have no configuration so we're going to try both parsing
// modes.
globals terraform . InputValues
files terraform . InputValues
value , diags := variable . ParseVariableValue ( configs . VariableParseHCL )
if ! diags . HasErrors ( ) {
// then good! we can just return these values directly.
return value , diags
}
values * VariableCaches // back reference so we can access the stored values
}
// otherwise, we'll try the other one.
// GetCache returns the cache for the named run. If the cache does not exist, it
// is created and returned.
func ( caches * VariableCaches ) GetCache ( name string , config * configs . Config ) * VariableCache {
caches . cacheLock . Lock ( )
defer caches . cacheLock . Unlock ( )
cache , exists := caches . caches [ name ]
if ! exists {
cache = & VariableCache {
config : config ,
globals : make ( terraform . InputValues ) ,
files : make ( terraform . InputValues ) ,
values : caches ,
value , diags = variable . ParseVariableValue ( configs . VariableParseLiteral )
if diags . HasErrors ( ) {
// we'll add a warning diagnostic here, just telling the users they
// can avoid this by adding a variable definition.
diags = diags . Append ( tfdiags . Sourceless (
tfdiags . Warning ,
"Missing variable definition" ,
fmt . Sprintf ( "The variable %q could not be parsed. Terraform had no definition block for this variable, this error could be avoided in future by including a definition block for this variable within the Terraform test file." , name ) ) )
// as usual make sure we still provide something for this value.
value = & terraform . InputValue {
Value : cty . DynamicVal ,
}
caches . caches [ name ] = cache
}
return cache
return value, diags
}
// GetGlobalVariable returns a value for the named global variable evaluated
// against the current run.
//
// This function caches the result of evaluating the variable so that it is
// only evaluated once per run.
//
// This function will return a valid input value if parsing fails for any reason
// so the caller can continue processing the configuration. The diagnostics
// returned will contain the error message that occurred during parsing and as
// such should be shown to the user.
func ( cache * VariableCache ) GetGlobalVariable ( name string ) ( * terraform . InputValue , tfdiags . Diagnostics ) {
val , exists := cache . globals [ name ]
if exists {
return val , nil
}
variable , exists := cache . values . GlobalVariables [ name ]
func ( cache * VariableCache ) evaluateVariableDefinition ( name string ) ( * terraform . InputValue , tfdiags . Diagnostics ) {
definition , exists := cache . TestFileVariableDefinitions [ name ]
if ! exists {
// no definition for this variable
return nil , nil
}
// TODO: We should also introduce a way to specify the mode in the test
// file itself. Suggestion, optional variable blocks.
parsingMode := configs . VariableParseHCL
if cfg , exists := cache . config . Module . Variables [ name ] ; exists {
parsingMode = cfg . ParsingMode
}
var diags tfdiags . Diagnostics
value , diags := variable . ParseVariableValue ( parsingMode )
if diags. HasErrors ( ) {
// In this case, the variable exists but we couldn't parse it. We'll
// return a usable value so that we don't compound errors later by
// claiming a variable doesn't exist when it does. We also return the
// diagnostics explaining the error which will be shown to the user.
value = & terraform . InputValue {
Value : cty . Dynamic Val,
var input * terraform . InputValue
if _ , exists := cache . ExternalVariableValues [ name ] ; exists {
parsed , moreDiags := cache . EvaluateExternalVariable ( name , definition )
diags = diags . Append ( moreDiags )
input = parsed
} else {
input = & terraform . InputValue {
Value : cty . NilVal ,
}
}
cache . globals [ name ] = value
return value , diags
value , moreDiags := terraform . PrepareFinalInputVariableValue ( addrs . AbsInputVariableInstance {
Module : addrs . RootModuleInstance ,
Variable : addrs . InputVariable {
Name : name ,
} ,
} , input , definition )
diags = diags . Append ( moreDiags )
return & terraform . InputValue {
Value : value ,
SourceType : terraform . ValueFromConfig ,
SourceRange : tfdiags . SourceRangeFromHCL ( definition . DeclRange ) ,
} , diags
}
// GetFileVariable returns a value for the named file-level variable evaluated
// against the current run.
//
// This function caches the result of evaluating the variable so that it is
// only evaluated once per run.
//
// This function will return a valid input value if parsing fails for any reason
// so the caller can continue processing the configuration. The diagnostics
// returned will contain the error message that occurred during parsing and as
// such should be shown to the user.
func ( cache * VariableCache ) GetFileVariable ( name string ) ( * terraform . InputValue , tfdiags . Diagnostics ) {
val , exists := cache . files [ name ]
if exists {
return val , nil
}
expr , exists := cache . values . FileVariables [ name ]
func ( cache * VariableCache ) evaluateFileVariable ( name string ) ( * terraform . InputValue , tfdiags . Diagnostics ) {
expr , exists := cache . TestFileVariableExpressions [ name ]
if ! exists {
return nil , nil
}
@ -146,9 +140,12 @@ func (cache *VariableCache) GetFileVariable(name string) (*terraform.InputValue,
refs , refDiags := langrefs . ReferencesInExpr ( addrs . ParseRefFromTestingScope , expr )
for _ , ref := range refs {
if input , ok := ref . Subject . ( addrs . InputVariable ) ; ok {
variable , variableDiags := cache . GetGlobalVariable ( input . Name )
diags = diags . Append ( variableDiags )
variable , variableDiags := cache . evaluateVariableDefinition ( input . Name )
if variable != nil {
diags = diags . Append ( variableDiags )
availableVariables [ input . Name ] = variable . Value
} else if variable , variableDiags := cache . EvaluateExternalVariable ( input . Name , nil ) ; variable != nil {
diags = diags . Append ( variableDiags )
availableVariables [ input . Name ] = variable . Value
}
}
@ -156,28 +153,17 @@ func (cache *VariableCache) GetFileVariable(name string) (*terraform.InputValue,
diags = diags . Append ( refDiags )
if diags . HasErrors ( ) {
// There's no point trying to evaluate the variable as we know it will
// fail. We'll just return a usable value so that we don't compound
// errors later by claiming a variable doesn't exist when it does. We
// also return the diagnostics explaining the error which will be shown
// to the user.
cache . files [ name ] = & terraform . InputValue {
return & terraform . InputValue {
Value : cty . DynamicVal ,
}
return cache . files [ name ] , diags
} , diags
}
ctx , ctxDiags := EvalContext ( TargetFileVariable , map [ string ] hcl . Expression { name : expr } , availableVariables , nil )
diags = diags . Append ( ctxDiags )
if ctxDiags . HasErrors ( ) {
// If we couldn't build the context, we won't actually process these
// variables. Instead, we'll fill them with an empty value but still
// make a note that the user did provide them.
cache . files [ name ] = & terraform . InputValue {
return & terraform . InputValue {
Value : cty . DynamicVal ,
}
return cache . files [ name ] , diags
} , diags
}
value , valueDiags := expr . Value ( ctx )
@ -190,10 +176,50 @@ func (cache *VariableCache) GetFileVariable(name string) (*terraform.InputValue,
value = cty . DynamicVal
}
cache . files [ name ] = & terraform . InputValue {
return & terraform . InputValue {
Value : value ,
SourceType : terraform . ValueFromConfig ,
SourceRange : tfdiags . SourceRangeFromHCL ( expr . Range ( ) ) ,
} , diags
}
func ( cache * VariableCache ) GetVariableValue ( name string ) ( * terraform . InputValue , tfdiags . Diagnostics ) {
cache . mutex . Lock ( )
defer cache . mutex . Unlock ( )
if cache . fileVariableValues == nil {
cache . fileVariableValues = make ( map [ string ] * terraform . InputValue )
}
if value , exists := cache . fileVariableValues [ name ] ; exists {
return value , nil
}
if value , valueDiags := cache . evaluateFileVariable ( name ) ; value != nil {
cache . fileVariableValues [ name ] = value
return value , valueDiags
}
if value , valueDiags := cache . evaluateVariableDefinition ( name ) ; value != nil {
cache . fileVariableValues [ name ] = value
return value , valueDiags
}
if value , valueDiags := cache . EvaluateExternalVariable ( name , nil ) ; value != nil {
cache . fileVariableValues [ name ] = value
return value , valueDiags
}
return nil , nil
}
func ( cache * VariableCache ) HasVariableDefinition ( name string ) bool {
if _ , exists := cache . TestFileVariableExpressions [ name ] ; exists {
return true
}
if _ , exists := cache . TestFileVariableDefinitions [ name ] ; exists {
return true
}
return cache . files [ name ] , diags
return false
}