blob: cc1298b16c1ddd1cb37fd625502d78899ad5ef28 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hcl
import (
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend/backendrun"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/lang/langrefs"
"github.com/hashicorp/terraform/internal/terraform"
"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
}
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{},
}
for _, opt := range opts {
opt(ret)
}
return ret
}
// 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
globals terraform.InputValues
files terraform.InputValues
values *VariableCaches // back reference so we can access the stored values
}
// 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,
}
caches.caches[name] = cache
}
return cache
}
// 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]
if !exists {
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
}
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.DynamicVal,
}
}
cache.globals[name] = value
return value, 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]
if !exists {
return nil, nil
}
var diags tfdiags.Diagnostics
availableVariables := make(map[string]cty.Value)
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)
if variable != nil {
availableVariables[input.Name] = variable.Value
}
}
}
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{
Value: cty.DynamicVal,
}
return cache.files[name], 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{
Value: cty.DynamicVal,
}
return cache.files[name], diags
}
value, valueDiags := expr.Value(ctx)
diags = diags.Append(valueDiags)
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 = cty.DynamicVal
}
cache.files[name] = &terraform.InputValue{
Value: value,
SourceType: terraform.ValueFromConfig,
SourceRange: tfdiags.SourceRangeFromHCL(expr.Range()),
}
return cache.files[name], diags
}