blob: d6b1ceb7f0ff12eaa5fa01295b3e53b2274269bd [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package graph
import (
"context"
"fmt"
"log"
"sort"
"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/backendrun"
"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/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// TestFileState is a helper struct that just maps a run block to the state that
// was produced by the execution of that run block.
type TestFileState struct {
Run *moduletest.Run
State *states.State
}
// 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]backendrun.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 used to clean up the infrastructure created during the test after
// the test has finished.
FileStates map[string]*TestFileState
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
evalSem terraform.Semaphore
}
type EvalContextOpts struct {
Verbose bool
Render views.Test
CancelCtx context.Context
StopCtx context.Context
UnparsedVariables map[string]backendrun.UnparsedVariableValue
Config *configs.Config
Concurrency int
}
// 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: make(map[string]*TestFileState),
stateLock: sync.Mutex{},
cancelContext: cancelCtx,
cancelFunc: cancel,
stopContext: stopCtx,
stopFunc: stop,
verbose: opts.Verbose,
renderer: opts.Render,
config: opts.Config,
evalSem: terraform.NewSemaphore(opts.Concurrency),
}
}
// 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 *moduletest.Run, resultScope *lang.Scope, extraVariableVals terraform.InputValues) (moduletest.Status, cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if run.ModuleConfig == nil {
// This should never happen, but if it does, we can't evaluate the run
return moduletest.Error, cty.NilVal, tfdiags.Diagnostics{}
}
mod := run.ModuleConfig.Module
// 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: mod,
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.Addr())
// We're going to assume the run has passed, and then if anything fails this
// value will be updated.
status := run.Status.Merge(moduletest.Pass)
// Now validate all the assertions within this run block.
for i, rule := range run.Config.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 evaulate 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.Addr())
status = status.Merge(moduletest.Error)
diags = diags.Append(ruleDiags)
continue
}
errorMessage, moreDiags := lang.EvalCheckErrorMessage(rule.ErrorMessage, hclCtx, nil)
ruleDiags = ruleDiags.Append(moreDiags)
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.Addr())
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.Addr())
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.Addr())
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.Addr())
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.Addr(), i)
status = status.Merge(moduletest.Fail)
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Test assertion failed",
Detail: errorMessage,
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.Addr(), 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(mod.Outputs))
runRng := tfdiags.SourceRangeFromHCL(run.Config.DeclRange)
for _, oc := range mod.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, state *TestFileState) {
ec.stateLock.Lock()
defer ec.stateLock.Unlock()
ec.FileStates[key] = &TestFileState{
Run: state.Run,
State: state.State,
}
}
func (ec *EvalContext) GetFileState(key string) *TestFileState {
ec.stateLock.Lock()
defer ec.stateLock.Unlock()
return ec.FileStates[key]
}
// 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
}
// 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
}