mirror of https://github.com/hashicorp/terraform
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
793 lines
28 KiB
793 lines
28 KiB
// 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
|
|
|
|
deferralAllowed 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
|
|
DeferralAllowed bool
|
|
}
|
|
|
|
// 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,
|
|
deferralAllowed: opts.DeferralAllowed,
|
|
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
|
|
}
|