terraform/internal/moduletest/graph/plan.go
2026-02-17 13:56:34 +00:00

147 lines
5.2 KiB
Go

// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
package graph
import (
"fmt"
"log"
"path/filepath"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// testPlan defines how to execute a run block representing a plan command
//
// See also: (n *NodeTestRun).testApply
func (n *NodeTestRun) testPlan(ctx *EvalContext, variables terraform.InputValues, providers map[addrs.RootProviderConfig]providers.Interface, waiter *operationWaiter) {
file, run := n.File(), n.run
config := run.ModuleConfig
// FilterVariablesToModule only returns warnings, so we don't check the
// returned diags for errors.
setVariables, testOnlyVariables, setVariableDiags := FilterVariablesToModule(run.ModuleConfig, variables)
run.Diagnostics = run.Diagnostics.Append(setVariableDiags)
// ignore diags because validate has covered it
tfCtx, _ := terraform.NewContext(n.opts.ContextOpts)
// execute the terraform plan operation
planScope, plan, originalDiags := plan(ctx, tfCtx, file.Config, run.Config, run.ModuleConfig, setVariables, providers, waiter)
// We exclude the diagnostics that are expected to fail from the plan
// diagnostics, and if an expected failure is not found, we add a new error diagnostic.
planDiags := moduletest.ValidateExpectedFailures(run.Config, originalDiags)
if ctx.Verbose() {
// in verbose mode, we still add all the original diagnostics for
// display.
run.Diagnostics = run.Diagnostics.Append(originalDiags)
} else {
run.Diagnostics = run.Diagnostics.Append(planDiags)
}
if planDiags.HasErrors() {
run.Status = moduletest.Error
return
}
if ctx.Verbose() {
schemas, diags := tfCtx.Schemas(config, plan.PriorState)
// If we're going to fail to render the plan, let's not fail the overall
// test. It can still have succeeded. So we'll add the diagnostics, but
// still report the test status as a success.
if diags.HasErrors() {
// This is very unlikely.
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Failed to print verbose output",
fmt.Sprintf("Terraform failed to print the verbose output for %s, other diagnostics will contain more details as to why.", filepath.Join(file.Name, run.Name))))
} else {
run.Verbose = &moduletest.Verbose{
Plan: plan,
State: nil, // We don't have a state to show in plan mode.
Config: config,
Providers: schemas.Providers,
Provisioners: schemas.Provisioners,
}
}
run.Diagnostics = run.Diagnostics.Append(diags)
}
// Evaluate the run block directly in the graph context to validate the assertions
// of the run. We also pass in all the
// previous contexts so this run block can refer to outputs from
// previous run blocks.
status, outputVals, moreDiags := ctx.EvaluateRun(run.Config, run.ModuleConfig.Module, planScope, testOnlyVariables)
run.Status = run.Status.Merge(status)
run.Diagnostics = run.Diagnostics.Append(moreDiags)
run.Outputs = outputVals
}
func plan(ctx *EvalContext, tfCtx *terraform.Context, file *configs.TestFile, run *configs.TestRun, module *configs.Config, variables terraform.InputValues, providers map[addrs.RootProviderConfig]providers.Interface, waiter *operationWaiter) (*lang.Scope, *plans.Plan, tfdiags.Diagnostics) {
log.Printf("[TRACE] TestFileRunner: called plan for %s", run.Name)
var diags tfdiags.Diagnostics
targets, targetDiags := moduletest.GetRunTargets(run)
diags = diags.Append(targetDiags)
replaces, replaceDiags := moduletest.GetRunReplaces(run)
diags = diags.Append(replaceDiags)
references, referenceDiags := moduletest.GetRunReferences(run)
diags = diags.Append(referenceDiags)
state, err := ctx.LoadState(run)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to load state",
Detail: fmt.Sprintf("Could not retrieve state for run %s: %s.", run.Name, err),
Subject: run.Backend.DeclRange.Ptr(),
})
}
if diags.HasErrors() {
return nil, nil, diags
}
planOpts := &terraform.PlanOpts{
Mode: func() plans.Mode {
switch run.Options.Mode {
case configs.RefreshOnlyTestMode:
return plans.RefreshOnlyMode
default:
return plans.NormalMode
}
}(),
Targets: targets,
ForceReplace: replaces,
SkipRefresh: !run.Options.Refresh,
SetVariables: variables,
ExternalReferences: references,
ExternalProviders: providers,
Overrides: ctx.GetOverrides(run.Name),
DeferralAllowed: ctx.deferralAllowed,
AllowRootEphemeralOutputs: true,
}
waiter.update(tfCtx, moduletest.Running, nil)
log.Printf("[DEBUG] TestFileRunner: starting plan for %s", run.Name)
plan, scope, planDiags := tfCtx.PlanAndEval(module, state, planOpts)
log.Printf("[DEBUG] TestFileRunner: completed plan for %s", run.Name)
diags = diags.Append(planDiags)
return scope, plan, diags
}