| // 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 |
| } |