// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package graph import ( "fmt" "log" "time" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/moduletest" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) var ( _ GraphNodeExecutable = (*NodeTestRun)(nil) ) type NodeTestRun struct { run *moduletest.Run opts *graphOptions } func (n *NodeTestRun) Run() *moduletest.Run { return n.run } func (n *NodeTestRun) File() *moduletest.File { return n.opts.File } func (n *NodeTestRun) Name() string { return fmt.Sprintf("%s.%s", n.opts.File.Name, n.run.Name) } func (n *NodeTestRun) References() []*addrs.Reference { references, _ := n.run.GetReferences() return references } // Execute executes the test run block and update the status of the run block // based on the result of the execution. func (n *NodeTestRun) Execute(evalCtx *EvalContext) tfdiags.Diagnostics { log.Printf("[TRACE] TestFileRunner: executing run block %s/%s", n.File().Name, n.run.Name) startTime := time.Now().UTC() var diags tfdiags.Diagnostics file, run := n.File(), n.run // At the end of the function, we'll update the status of the file based on // the status of the run block, and render the run summary. defer func() { evalCtx.Renderer().Run(run, file, moduletest.Complete, 0) file.UpdateStatus(run.Status) }() if file.GetStatus() == moduletest.Error { // If the overall test file has errored, we don't keep trying to // execute tests. Instead, we mark all remaining run blocks as // skipped, print the status, and move on. run.Status = moduletest.Skip return diags } if evalCtx.Cancelled() { // A cancellation signal has been received. // Don't do anything, just give up and return immediately. // The surrounding functions should stop this even being called, but in // case of race conditions or something we can still verify this. return diags } if evalCtx.Stopped() { // Then the test was requested to be stopped, so we just mark each // following test as skipped, print the status, and move on. run.Status = moduletest.Skip return diags } // Create a waiter which handles waiting for terraform operations to complete. // While waiting, the wait will also respond to cancellation signals, and // handle them appropriately. // The test progress is updated periodically, and the progress status // depends on the async operation being waited on. // Before the terraform operation is started, the operation updates the // waiter with the cleanup context on cancellation, as well as the // progress status. waiter := NewOperationWaiter(nil, evalCtx, n, moduletest.Running, startTime.UnixMilli()) cancelled := waiter.Run(func() { defer logging.PanicHandler() n.execute(evalCtx, waiter) }) if cancelled { diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Test interrupted", "The test operation could not be completed due to an interrupt signal. Please read the remaining diagnostics carefully for any sign of failed state cleanup or dangling resources.")) } // If we got far enough to actually attempt to execute the run then // we'll give the view some additional metadata about the execution. n.run.ExecutionMeta = &moduletest.RunExecutionMeta{ Start: startTime, Duration: time.Since(startTime), } return diags } func (n *NodeTestRun) execute(ctx *EvalContext, waiter *operationWaiter) { file, run := n.File(), n.run ctx.Renderer().Run(run, file, moduletest.Starting, 0) if run.Config.ConfigUnderTest != nil && run.GetStateKey() == moduletest.MainStateIdentifier { // This is bad, and should not happen because the state key is derived from the custom module source. panic(fmt.Sprintf("TestFileRunner: custom module %s has the same key as main state", file.Name)) } n.testValidate(ctx, waiter) if run.Diagnostics.HasErrors() { return } variables, variableDiags := n.GetVariables(ctx, true) run.Diagnostics = run.Diagnostics.Append(variableDiags) if variableDiags.HasErrors() { run.Status = moduletest.Error return } if run.Config.Command == configs.PlanTestCommand { n.testPlan(ctx, variables, waiter) } else { n.testApply(ctx, variables, waiter) } } // Validating the module config which the run acts on func (n *NodeTestRun) testValidate(ctx *EvalContext, waiter *operationWaiter) { run := n.run file := n.File() config := run.ModuleConfig log.Printf("[TRACE] TestFileRunner: called validate for %s/%s", file.Name, run.Name) TransformConfigForRun(ctx, run, file) tfCtx, ctxDiags := terraform.NewContext(n.opts.ContextOpts) if ctxDiags.HasErrors() { return } waiter.update(tfCtx, moduletest.Running, nil) validateDiags := tfCtx.Validate(config, nil) run.Diagnostics = run.Diagnostics.Append(validateDiags) if validateDiags.HasErrors() { run.Status = moduletest.Error return } }