terraform/internal/moduletest/graph/eval_context.go
Daniel Schmidt 026c935961 move UnparsedVariableValue from backendrun to arguments
This prevents a cyclic dependency and also makes sense semantically.
The arguments package will collect the unparsed variable values and
the backendrun helpers will work to collect the values and transform
them into terraform.InputValue.
2026-02-18 12:47:12 +01:00

883 lines
31 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package graph
import (
"context"
"fmt"
"log"
"sort"
"strings"
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/backend"
"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/views"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/didyoumean"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/lang/langrefs"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/moduletest/mocking"
teststates "github.com/hashicorp/terraform/internal/moduletest/states"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// EvalContext is a container for context relating to the evaluation of a
// particular .tftest.hcl file.
// This context is used to track the various values that are available to the
// test suite, both from the test suite itself and from the results of the runs
// within the suite.
// The struct provides concurrency-safe access to the various maps it contains.
type EvalContext struct {
// unparsedVariables and parsedVariables are the values for the variables
// required by this test file. The parsedVariables will be populated as the
// test graph is executed, while the unparsedVariables will be lazily
// evaluated by each run block that needs them.
unparsedVariables map[string]arguments.UnparsedVariableValue
parsedVariables terraform.InputValues
variableStatus map[string]moduletest.Status
variablesLock sync.Mutex
// runBlocks caches all the known run blocks that this EvalContext manages.
runBlocks map[string]*moduletest.Run
outputsLock sync.Mutex
providers map[addrs.RootProviderConfig]providers.Interface
providerStatus map[addrs.RootProviderConfig]moduletest.Status
providersLock sync.Mutex
// FileStates is a mapping of module keys to it's last applied state
// file. This is tracked and returned to log state files of ongoing test
// operations.
FileStates map[string]*teststates.TestRunState
stateLock sync.Mutex
// cancelContext and stopContext can be used to terminate the evaluation of the
// test suite when a cancellation or stop signal is received.
// cancelFunc and stopFunc are the corresponding functions to call to signal
// the termination.
cancelContext context.Context
cancelFunc context.CancelFunc
stopContext context.Context
stopFunc context.CancelFunc
config *configs.Config
renderer views.Test
verbose bool
// mode and repair affect the behaviour of the cleanup process of the graph.
//
// in cleanup mode, the tests will actually be skipped and the cleanup nodes
// are executed immediately. Normally, the skip_cleanup attributes will
// be skipped in cleanup mode with all states being destroyed completely.
//
// in repair mode, the skip_cleanup attributes are still respected. this
// means only states that were left behind due to an error will be
// destroyed.
mode moduletest.CommandMode
deferralAllowed bool
evalSem terraform.Semaphore
// repair is true if the test suite is being run in cleanup repair mode.
// It is only set when in test cleanup mode.
repair bool
overrides map[string]*mocking.Overrides
overrideLock sync.Mutex
}
type EvalContextOpts struct {
Verbose bool
Repair bool
Render views.Test
CancelCtx context.Context
StopCtx context.Context
UnparsedVariables map[string]arguments.UnparsedVariableValue
Config *configs.Config
FileStates map[string]*teststates.TestRunState
Concurrency int
DeferralAllowed bool
Mode moduletest.CommandMode
}
// NewEvalContext constructs a new graph evaluation context for use in
// evaluating the runs within a test suite.
// The context is initialized with the provided cancel and stop contexts, and
// these contexts can be used from external commands to signal the termination of the test suite.
func NewEvalContext(opts EvalContextOpts) *EvalContext {
cancelCtx, cancel := context.WithCancel(opts.CancelCtx)
stopCtx, stop := context.WithCancel(opts.StopCtx)
return &EvalContext{
unparsedVariables: opts.UnparsedVariables,
parsedVariables: make(terraform.InputValues),
variableStatus: make(map[string]moduletest.Status),
variablesLock: sync.Mutex{},
runBlocks: make(map[string]*moduletest.Run),
outputsLock: sync.Mutex{},
providers: make(map[addrs.RootProviderConfig]providers.Interface),
providerStatus: make(map[addrs.RootProviderConfig]moduletest.Status),
providersLock: sync.Mutex{},
FileStates: opts.FileStates,
stateLock: sync.Mutex{},
cancelContext: cancelCtx,
cancelFunc: cancel,
stopContext: stopCtx,
stopFunc: stop,
config: opts.Config,
verbose: opts.Verbose,
repair: opts.Repair,
renderer: opts.Render,
mode: opts.Mode,
deferralAllowed: opts.DeferralAllowed,
evalSem: terraform.NewSemaphore(opts.Concurrency),
overrides: make(map[string]*mocking.Overrides),
}
}
// Renderer returns the renderer for the test suite.
func (ec *EvalContext) Renderer() views.Test {
return ec.renderer
}
// Cancel signals to the runs in the test suite that they should stop evaluating
// the test suite, and return immediately.
func (ec *EvalContext) Cancel() {
ec.cancelFunc()
}
// Cancelled returns true if the context has been stopped. The default cause
// of the error is context.Canceled.
func (ec *EvalContext) Cancelled() bool {
return ec.cancelContext.Err() != nil
}
// Stop signals to the runs in the test suite that they should stop evaluating
// the test suite, and just skip.
func (ec *EvalContext) Stop() {
ec.stopFunc()
}
func (ec *EvalContext) Stopped() bool {
return ec.stopContext.Err() != nil
}
// Verbose returns true if the context is in verbose mode.
func (ec *EvalContext) Verbose() bool {
return ec.verbose
}
func (ec *EvalContext) HclContext(references []*addrs.Reference) (*hcl.EvalContext, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
runs := make(map[string]cty.Value)
vars := make(map[string]cty.Value)
for _, reference := range references {
switch subject := reference.Subject.(type) {
case addrs.Run:
run, ok := ec.GetOutput(subject.Name)
if !ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unknown run block",
Detail: fmt.Sprintf("The run block %q does not exist within this test file.", subject.Name),
Subject: reference.SourceRange.ToHCL().Ptr(),
})
continue
}
runs[subject.Name] = run
value, valueDiags := reference.Remaining.TraverseRel(run)
diags = diags.Append(valueDiags)
if valueDiags.HasErrors() {
continue
}
if !value.IsWhollyKnown() {
// This is not valid, we cannot allow users to pass unknown
// values into references within the test file. There's just
// going to be difficult and confusing errors later if this
// happens.
//
// When reporting this we assume that it's happened because
// the prior run was a plan-only run and that some of its
// output values were not known. If this arises for a
// run that performed a full apply then this is a bug in
// Terraform's modules runtime, because unknown output
// values should not be possible in that case.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unknown value",
Detail: fmt.Sprintf("The value for %s is unknown. Run block %q is executing a \"plan\" operation, and the specified output value is only known after apply.", reference.DisplayString(), subject.Name),
Subject: reference.SourceRange.ToHCL().Ptr(),
})
continue
}
case addrs.InputVariable:
if variable, ok := ec.GetVariable(subject.Name); ok {
vars[subject.Name] = variable.Value
continue
}
if variable, moreDiags := ec.EvaluateUnparsedVariableDeprecated(subject.Name, reference); variable != nil {
diags = diags.Append(moreDiags)
vars[subject.Name] = variable.Value
continue
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unavailable variable",
Detail: fmt.Sprintf("The input variable %q does not exist within this test file.", subject.Name),
Subject: reference.SourceRange.ToHCL().Ptr(),
})
continue
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid reference",
Detail: "You can only reference run blocks and variables from within Terraform Test files.",
Subject: reference.SourceRange.ToHCL().Ptr(),
})
}
}
return &hcl.EvalContext{
Variables: map[string]cty.Value{
"run": cty.ObjectVal(runs),
"var": cty.ObjectVal(vars),
},
Functions: lang.TestingFunctions(),
}, diags
}
// EvaluateRun processes the assertions inside the provided configs.TestRun against
// the run results, returning a status, an object value representing the output
// values from the module under test, and diagnostics describing any problems.
//
// extraVariableVals, if provided, overlays the input variables that are
// already available in resultScope in case there are additional input
// variables that were defined only for use in the test suite. Any variable
// not defined in extraVariableVals will be evaluated through resultScope instead.
func (ec *EvalContext) EvaluateRun(run *configs.TestRun, module *configs.Module, resultScope *lang.Scope, extraVariableVals terraform.InputValues) (moduletest.Status, cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// We need a derived evaluation scope that also supports referring to
// the prior run output values using the "run.NAME" syntax.
evalData := &evaluationData{
ctx: ec,
module: module,
current: resultScope.Data,
extraVars: extraVariableVals,
}
scope := &lang.Scope{
Data: evalData,
ParseRef: addrs.ParseRefFromTestingScope,
SourceAddr: resultScope.SourceAddr,
BaseDir: resultScope.BaseDir,
PureOnly: resultScope.PureOnly,
PlanTimestamp: resultScope.PlanTimestamp,
ExternalFuncs: resultScope.ExternalFuncs,
}
log.Printf("[TRACE] EvalContext.Evaluate for %s", run.Name)
// We're going to assume the run has passed, and then if anything fails this
// value will be updated.
status := moduletest.Pass
// Now validate all the assertions within this run block.
for i, rule := range run.CheckRules {
var ruleDiags tfdiags.Diagnostics
refs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.Condition)
ruleDiags = ruleDiags.Append(moreDiags)
moreRefs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.ErrorMessage)
ruleDiags = ruleDiags.Append(moreDiags)
refs = append(refs, moreRefs...)
// We want to emit diagnostics if users are using ephemeral resources in their checks
// as they are not supported since they are closed before this is evaluated.
// We do not remove the diagnostic about the ephemeral resource being closed already as it
// might be useful to the user.
ruleDiags = ruleDiags.Append(diagsForEphemeralResources(refs))
hclCtx, moreDiags := scope.EvalContext(refs)
ruleDiags = ruleDiags.Append(moreDiags)
if moreDiags.HasErrors() {
// if we can't evaluate the context properly, we can't evaluate the rule
// we add the diagnostics to the main diags and continue to the next rule
log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s is invalid, could not evalaute the context, so cannot evaluate it", i, run.Name)
status = status.Merge(moduletest.Error)
diags = diags.Append(ruleDiags)
continue
}
errorMessage, moreDiags := lang.EvalCheckErrorMessage(rule.ErrorMessage, hclCtx, nil)
ruleDiags = ruleDiags.Append(moreDiags)
errorMessage, _ = errorMessage.Unmark()
errorMessageStr := strings.TrimSpace(errorMessage.AsString())
runVal, hclDiags := rule.Condition.Value(hclCtx)
ruleDiags = ruleDiags.Append(hclDiags)
diags = diags.Append(ruleDiags)
if ruleDiags.HasErrors() {
log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s is invalid, so cannot evaluate it", i, run.Name)
status = status.Merge(moduletest.Error)
continue
}
if runVal.IsNull() {
status = status.Merge(moduletest.Error)
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid condition run",
Detail: "Condition expression must return either true or false, not null.",
Subject: rule.Condition.Range().Ptr(),
Expression: rule.Condition,
EvalContext: hclCtx,
})
log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s has null condition result", i, run.Name)
continue
}
if !runVal.IsKnown() {
status = status.Merge(moduletest.Error)
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Unknown condition value",
Detail: "Condition expression could not be evaluated at this time. This means you have executed a `run` block with `command = plan` and one of the values your condition depended on is not known until after the plan has been applied. Either remove this value from your condition, or execute an `apply` command from this `run` block. Alternatively, if there is an override for this value, you can make it available during the plan phase by setting `override_during = plan` in the `override_` block.",
Subject: rule.Condition.Range().Ptr(),
Expression: rule.Condition,
EvalContext: hclCtx,
})
log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s has unknown condition result", i, run.Name)
continue
}
var err error
if runVal, err = convert.Convert(runVal, cty.Bool); err != nil {
status = status.Merge(moduletest.Error)
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid condition run",
Detail: fmt.Sprintf("Invalid condition run value: %s.", tfdiags.FormatError(err)),
Subject: rule.Condition.Range().Ptr(),
Expression: rule.Condition,
EvalContext: hclCtx,
})
log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s has non-boolean condition result", i, run.Name)
continue
}
// If the runVal refers to any sensitive values, then we'll have a
// sensitive mark on the resulting value.
runVal, _ = runVal.Unmark()
if runVal.False() {
log.Printf("[TRACE] EvalContext.Evaluate: test assertion failed for %s assertion %d", run.Name, i)
status = status.Merge(moduletest.Fail)
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Test assertion failed",
Detail: errorMessageStr,
Subject: rule.Condition.Range().Ptr(),
Expression: rule.Condition,
EvalContext: hclCtx,
// Diagnostic can be identified as originating from a failing test assertion.
// Also, values that are ephemeral, sensitive, or unknown are replaced with
// redacted values in renderings of the diagnostic.
Extra: DiagnosticCausedByTestFailure{Verbose: ec.verbose},
})
continue
} else {
log.Printf("[TRACE] EvalContext.Evaluate: test assertion succeeded for %s assertion %d", run.Name, i)
}
}
// Our result includes an object representing all of the output values
// from the module we've just tested, which will then be available in
// any subsequent test cases in the same test suite.
outputVals := make(map[string]cty.Value, len(module.Outputs))
runRng := tfdiags.SourceRangeFromHCL(run.DeclRange)
for _, oc := range module.Outputs {
addr := oc.Addr()
v, moreDiags := scope.Data.GetOutput(addr, runRng)
diags = diags.Append(moreDiags)
if v == cty.NilVal {
v = cty.NullVal(cty.DynamicPseudoType)
}
outputVals[addr.Name] = v
}
return status, cty.ObjectVal(outputVals), diags
}
// EvaluateUnparsedVariable accepts a variable name and a variable definition
// and checks if we have external unparsed variables that match the given
// configuration. If no variable was provided, we'll return a nil
// input value.
func (ec *EvalContext) EvaluateUnparsedVariable(name string, config *configs.Variable) (*terraform.InputValue, tfdiags.Diagnostics) {
variable, exists := ec.unparsedVariables[name]
if !exists {
return nil, nil
}
value, diags := variable.ParseVariableValue(config.ParsingMode)
if diags.HasErrors() {
value = &terraform.InputValue{
Value: cty.DynamicVal,
}
}
return value, diags
}
// EvaluateUnparsedVariableDeprecated accepts a variable name without a variable
// definition and attempts to parse it.
//
// This function represents deprecated functionality within the testing
// framework. It is no longer valid to reference external variables without a
// definition, but we do our best here and provide a warning that this will
// become completely unsupported in the future.
func (ec *EvalContext) EvaluateUnparsedVariableDeprecated(name string, ref *addrs.Reference) (*terraform.InputValue, tfdiags.Diagnostics) {
variable, exists := ec.unparsedVariables[name]
if !exists {
return nil, nil
}
var diags tfdiags.Diagnostics
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: "Variable referenced without definition",
Detail: fmt.Sprintf("Variable %q was referenced without providing a definition. Referencing undefined variables within Terraform Test files is deprecated, please add a `variable` block into the relevant test file to provide a definition for the variable. This will become required in future versions of Terraform.", name),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
// 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.
// Otherwise, we have no configuration so we're going to try both parsing
// modes.
value, moreDiags := variable.ParseVariableValue(configs.VariableParseHCL)
diags = diags.Append(moreDiags)
if !moreDiags.HasErrors() {
// then good! we can just return these values directly.
return value, diags
}
// otherwise, we'll try the other one.
value, moreDiags = variable.ParseVariableValue(configs.VariableParseLiteral)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
// as usual make sure we still provide something for this value.
value = &terraform.InputValue{
Value: cty.DynamicVal,
}
}
return value, diags
}
func (ec *EvalContext) SetVariable(name string, val *terraform.InputValue) {
ec.variablesLock.Lock()
defer ec.variablesLock.Unlock()
ec.parsedVariables[name] = val
}
func (ec *EvalContext) GetVariable(name string) (*terraform.InputValue, bool) {
ec.variablesLock.Lock()
defer ec.variablesLock.Unlock()
variable, ok := ec.parsedVariables[name]
return variable, ok
}
func (ec *EvalContext) SetVariableStatus(address string, status moduletest.Status) {
ec.variablesLock.Lock()
defer ec.variablesLock.Unlock()
ec.variableStatus[address] = status
}
func (ec *EvalContext) AddRunBlock(run *moduletest.Run) {
ec.outputsLock.Lock()
defer ec.outputsLock.Unlock()
ec.runBlocks[run.Name] = run
}
func (ec *EvalContext) GetOutput(name string) (cty.Value, bool) {
ec.outputsLock.Lock()
defer ec.outputsLock.Unlock()
output, ok := ec.runBlocks[name]
if !ok {
return cty.NilVal, false
}
return output.Outputs, true
}
func (ec *EvalContext) ProviderForConfigAddr(addr addrs.LocalProviderConfig) addrs.Provider {
return ec.config.ProviderForConfigAddr(addr)
}
func (ec *EvalContext) LocalNameForProvider(addr addrs.RootProviderConfig) string {
return ec.config.Module.LocalNameForProvider(addr.Provider)
}
func (ec *EvalContext) GetProvider(addr addrs.RootProviderConfig) (providers.Interface, bool) {
ec.providersLock.Lock()
defer ec.providersLock.Unlock()
provider, ok := ec.providers[addr]
return provider, ok
}
func (ec *EvalContext) SetProvider(addr addrs.RootProviderConfig, provider providers.Interface) {
ec.providersLock.Lock()
defer ec.providersLock.Unlock()
ec.providers[addr] = provider
}
func (ec *EvalContext) SetProviderStatus(addr addrs.RootProviderConfig, status moduletest.Status) {
ec.providersLock.Lock()
defer ec.providersLock.Unlock()
ec.providerStatus[addr] = status
}
func diagsForEphemeralResources(refs []*addrs.Reference) (diags tfdiags.Diagnostics) {
for _, ref := range refs {
switch v := ref.Subject.(type) {
case addrs.ResourceInstance:
if v.Resource.Mode == addrs.EphemeralResourceMode {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ephemeral resources cannot be asserted",
Detail: "Ephemeral resources are closed when the test is finished, and are not available within the test context for assertions.",
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
}
}
return diags
}
func (ec *EvalContext) SetFileState(key string, run *moduletest.Run, state *states.State, reason teststates.StateReason) {
ec.stateLock.Lock()
defer ec.stateLock.Unlock()
current := ec.getState(key)
// Whatever happens we're going to record the latest state for this key.
current.State = state
current.Manifest.Reason = reason
if run.Config.SkipCleanup {
// if skip cleanup is set on the run block, we're going to track it
// as the thing to target regardless of what else might be true.
current.Run = run
// we'll mark the state as being restored to the current run block
// if (a) we're not in cleanup mode (meaning everything should be
// destroyed) or (b) we are in cleanup mode but with the repair flag
// which means that only errored states should be destroyed.
current.RestoreState = ec.mode != moduletest.CleanupMode || ec.repair
} else if !current.RestoreState {
// otherwise, only set the new run block if we haven't been told the
// earlier run block is more relevant.
current.Run = run
}
}
// GetState retrieves the current state for the specified key, exactly as it
// specified within the current cache.
func (ec *EvalContext) GetState(key string) *teststates.TestRunState {
ec.stateLock.Lock()
defer ec.stateLock.Unlock()
return ec.getState(key)
}
func (ec *EvalContext) getState(key string) *teststates.TestRunState {
current := ec.FileStates[key]
if current == nil {
// this shouldn't happen, all the states must be initialised prior to
// the evaluation context being created.
//
// panic here, where the origin of the bug is instead of returning a
// null state to panic later.
panic("null state found in test execution")
}
return current
}
// LoadState returns the correct state for the specified run block. This differs
// from GetState in that it will load the state from any remote backend
// specified within the run block rather than simply retrieve the cached state
// (which might be empty for a run block with a backend if it hasn't executed
// yet).
func (ec *EvalContext) LoadState(run *configs.TestRun) (*states.State, error) {
ec.stateLock.Lock()
defer ec.stateLock.Unlock()
current := ec.getState(run.StateKey)
if run.Backend != nil {
// Then we'll load the state from the backend instead of just using
// whatever was in the state.
stmgr, sDiags := current.Backend.StateMgr(backend.DefaultStateName)
if sDiags.HasErrors() {
return nil, sDiags.Err()
}
if err := stmgr.RefreshState(); err != nil {
return nil, err
}
return stmgr.State(), nil
}
return current.State, nil
}
// ReferencesCompleted returns true if all the listed references were actually
// executed successfully. This allows nodes in the graph to decide if they
// should execute or not based on the status of their references.
func (ec *EvalContext) ReferencesCompleted(refs []*addrs.Reference) bool {
for _, ref := range refs {
switch ref := ref.Subject.(type) {
case addrs.Run:
ec.outputsLock.Lock()
if run, ok := ec.runBlocks[ref.Name]; ok {
if run.Status != moduletest.Pass && run.Status != moduletest.Fail {
ec.outputsLock.Unlock()
// see also prior runs completed
return false
}
}
ec.outputsLock.Unlock()
case addrs.InputVariable:
ec.variablesLock.Lock()
if vStatus, ok := ec.variableStatus[ref.Name]; ok && (vStatus == moduletest.Skip || vStatus == moduletest.Error) {
ec.variablesLock.Unlock()
return false
}
ec.variablesLock.Unlock()
}
}
return true
}
// ProvidersCompleted ensures that all required providers were properly
// initialised.
func (ec *EvalContext) ProvidersCompleted(providers map[addrs.RootProviderConfig]providers.Interface) bool {
ec.providersLock.Lock()
defer ec.providersLock.Unlock()
for provider := range providers {
if status, ok := ec.providerStatus[provider]; ok {
if status == moduletest.Skip || status == moduletest.Error {
return false
}
}
}
return true
}
// PriorRunsCompleted checks a list of run blocks against our internal log of
// completed run blocks and makes sure that any that do exist successfully
// executed to completion.
//
// Note that run blocks that are not in the list indicate a bad reference,
// which we ignore here. This is actually the problem of the caller to identify
// and error.
func (ec *EvalContext) PriorRunsCompleted(runs map[string]*moduletest.Run) bool {
ec.outputsLock.Lock()
defer ec.outputsLock.Unlock()
for name := range runs {
if run, ok := ec.runBlocks[name]; ok {
if run.Status != moduletest.Pass && run.Status != moduletest.Fail {
// pass and fail indicate the run block still executed the plan
// or apply operate and wrote outputs. fail means the
// post-execution checks failed, but we still had data to check.
// this is in contrast to pending, skip, or error which indicate
// that we never even wrote data for this run block.
return false
}
}
}
return true
}
func (ec *EvalContext) SetOverrides(run *moduletest.Run, overrides *mocking.Overrides) {
ec.overrideLock.Lock()
defer ec.overrideLock.Unlock()
ec.overrides[run.Name] = overrides
}
func (ec *EvalContext) GetOverrides(runName string) *mocking.Overrides {
ec.overrideLock.Lock()
defer ec.overrideLock.Unlock()
return ec.overrides[runName]
}
// evaluationData augments an underlying lang.Data -- presumably resulting
// from a terraform.Context.PlanAndEval or terraform.Context.ApplyAndEval call --
// with results from prior runs that should therefore be available when
// evaluating expressions written inside a "run" block.
type evaluationData struct {
ctx *EvalContext
module *configs.Module
current lang.Data
extraVars terraform.InputValues
}
var _ lang.Data = (*evaluationData)(nil)
// GetCheckBlock implements lang.Data.
func (d *evaluationData) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.current.GetCheckBlock(addr, rng)
}
// GetCountAttr implements lang.Data.
func (d *evaluationData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.current.GetCountAttr(addr, rng)
}
// GetForEachAttr implements lang.Data.
func (d *evaluationData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.current.GetForEachAttr(addr, rng)
}
// GetInputVariable implements lang.Data.
func (d *evaluationData) GetInputVariable(addr addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
if extra, exists := d.extraVars[addr.Name]; exists {
return extra.Value, nil
}
return d.current.GetInputVariable(addr, rng)
}
// GetLocalValue implements lang.Data.
func (d *evaluationData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.current.GetLocalValue(addr, rng)
}
// GetModule implements lang.Data.
func (d *evaluationData) GetModule(addr addrs.ModuleCall, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.current.GetModule(addr, rng)
}
// GetOutput implements lang.Data.
func (d *evaluationData) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.current.GetOutput(addr, rng)
}
// GetPathAttr implements lang.Data.
func (d *evaluationData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.current.GetPathAttr(addr, rng)
}
// GetResource implements lang.Data.
func (d *evaluationData) GetResource(addr addrs.Resource, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.current.GetResource(addr, rng)
}
// GetRunBlock implements lang.Data.
func (d *evaluationData) GetRunBlock(addr addrs.Run, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
ret, exists := d.ctx.GetOutput(addr.Name)
if !exists {
ret = cty.DynamicVal
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to undeclared run block",
Detail: fmt.Sprintf("There is no run %q declared in this test suite.", addr.Name),
Subject: rng.ToHCL().Ptr(),
})
}
if ret == cty.NilVal {
// An explicit nil value indicates that the block was declared but
// hasn't yet been visited.
ret = cty.DynamicVal
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unevaluated run block",
Detail: fmt.Sprintf("The run %q block has not yet been evaluated, so its results are not available here.", addr.Name),
Subject: rng.ToHCL().Ptr(),
})
}
return ret, diags
}
// GetTerraformAttr implements lang.Data.
func (d *evaluationData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
return d.current.GetTerraformAttr(addr, rng)
}
// StaticValidateReferences implements lang.Data.
func (d *evaluationData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics {
// We only handle addrs.Run directly here, with everything else delegated
// to the underlying Data object to deal with.
var diags tfdiags.Diagnostics
for _, ref := range refs {
switch ref.Subject.(type) {
case addrs.Run:
diags = diags.Append(d.staticValidateRunRef(ref))
default:
diags = diags.Append(d.current.StaticValidateReferences([]*addrs.Reference{ref}, self, source))
}
}
return diags
}
func (d *evaluationData) staticValidateRunRef(ref *addrs.Reference) tfdiags.Diagnostics {
d.ctx.outputsLock.Lock()
defer d.ctx.outputsLock.Unlock()
var diags tfdiags.Diagnostics
addr := ref.Subject.(addrs.Run)
if _, exists := d.ctx.runBlocks[addr.Name]; !exists {
var suggestions []string
for altAddr := range d.ctx.runBlocks {
suggestions = append(suggestions, altAddr)
}
sort.Strings(suggestions)
suggestion := didyoumean.NameSuggestion(addr.Name, suggestions)
if suggestion != "" {
suggestion = fmt.Sprintf(" Did you mean %q?", suggestion)
}
// A totally absent priorVals means that there is no run block with
// the given name at all. If it was declared but hasn't yet been
// evaluated then it would have an entry set to cty.NilVal.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to undeclared run block",
Detail: fmt.Sprintf("There is no run %q declared in this test suite.%s", addr.Name, suggestion),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
return diags
}