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

224 lines
8.8 KiB
Go

// Copyright IBM Corp. 2014, 2026
// 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/moduletest"
teststates "github.com/hashicorp/terraform/internal/moduletest/states"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/terraform"
"github.com/hashicorp/terraform/internal/tfdiags"
)
var (
_ GraphNodeExecutable = (*NodeStateCleanup)(nil)
)
// NodeStateCleanup is responsible for cleaning up the state of resources
// defined in the state file. It uses the provided stateKey to identify the
// specific state to clean up and opts for additional configuration options.
type NodeStateCleanup struct {
stateKey string
opts *graphOptions
}
func (n *NodeStateCleanup) Name() string {
return fmt.Sprintf("cleanup.%s", n.stateKey)
}
// Execute destroys the resources created in the state file.
func (n *NodeStateCleanup) Execute(evalCtx *EvalContext) {
file := n.opts.File
state := evalCtx.GetState(n.stateKey)
log.Printf("[TRACE] TestStateManager: cleaning up state for %s", file.Name)
if evalCtx.Cancelled() {
// Don't try and clean anything up if the execution has been cancelled.
log.Printf("[DEBUG] TestStateManager: skipping state cleanup for %s due to cancellation", file.Name)
return
}
if emptyState(state.State) {
// The state can be empty for a run block that just executed a plan
// command, or a run block that only read data sources. We'll just
// skip empty run blocks. We do reset the state reason to None for this
// just to make sure we are indicating externally this state file
// doesn't need to be saved.
evalCtx.SetFileState(n.stateKey, state.Run, state.State, teststates.StateReasonNone)
return
}
if state.Run == nil {
log.Printf("[ERROR] TestFileRunner: found inconsistent run block and state file in %s for module %s", file.Name, n.stateKey)
// The state can have a nil run block if it only executed a plan
// command. In which case, we shouldn't have reached here as the
// state should also have been empty and this will have been skipped
// above. If we do reach here, then something has gone badly wrong
// and we can't really recover from it.
diags := tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "Inconsistent state", fmt.Sprintf("Found inconsistent state while cleaning up %s. This is a bug in Terraform - please report it", file.Name))}
file.UpdateStatus(moduletest.Error)
evalCtx.Renderer().DestroySummary(diags, nil, file, state.State)
return
}
updated := state.State
startTime := time.Now().UTC()
waiter := NewOperationWaiter(nil, evalCtx, file, state.Run, moduletest.Running, startTime.UnixMilli())
var destroyDiags tfdiags.Diagnostics
evalCtx.Renderer().Run(state.Run, file, moduletest.TearDown, 0)
cancelled := waiter.Run(func() {
if state.RestoreState {
updated, destroyDiags = n.restore(evalCtx, file.Config, state.Run.Config, state.Run.ModuleConfig, updated, waiter)
} else {
updated, destroyDiags = n.destroy(evalCtx, file.Config, state.Run.Config, state.Run.ModuleConfig, updated, waiter)
updated.RootOutputValues = state.State.RootOutputValues // we're going to preserve the output values in case we need to tidy up
}
})
if cancelled {
destroyDiags = destroyDiags.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."))
}
switch {
case destroyDiags.HasErrors():
file.UpdateStatus(moduletest.Error)
evalCtx.SetFileState(n.stateKey, state.Run, updated, teststates.StateReasonError)
case state.Run.Config.Backend != nil:
evalCtx.SetFileState(n.stateKey, state.Run, updated, teststates.StateReasonNone)
case state.RestoreState:
evalCtx.SetFileState(n.stateKey, state.Run, updated, teststates.StateReasonSkip)
case !emptyState(updated):
file.UpdateStatus(moduletest.Error)
evalCtx.SetFileState(n.stateKey, state.Run, updated, teststates.StateReasonError)
default:
evalCtx.SetFileState(n.stateKey, state.Run, updated, teststates.StateReasonNone)
}
evalCtx.Renderer().DestroySummary(destroyDiags, state.Run, file, updated)
}
func (n *NodeStateCleanup) restore(ctx *EvalContext, file *configs.TestFile, run *configs.TestRun, module *configs.Config, state *states.State, waiter *operationWaiter) (*states.State, tfdiags.Diagnostics) {
log.Printf("[TRACE] TestFileRunner: called restore for %s", run.Name)
variables, diags := GetVariables(ctx, run, module, false)
if diags.HasErrors() {
return state, diags
}
// we ignore the diagnostics from here, because we will have reported them
// during the initial execution of the run block and we would not have
// executed the run block if there were any errors.
providers, _, _ := getProviders(ctx, file, run, module)
// During the destroy operation, we don't add warnings from this operation.
// Anything that would have been reported here was already reported during
// the original plan, and a successful destroy operation is the only thing
// we care about.
setVariables, _, _ := FilterVariablesToModule(module, variables)
planOpts := &terraform.PlanOpts{
Mode: plans.NormalMode,
SetVariables: setVariables,
Overrides: ctx.GetOverrides(run.Name),
ExternalProviders: providers,
SkipRefresh: true,
OverridePreventDestroy: true,
DeferralAllowed: ctx.deferralAllowed,
AllowRootEphemeralOutputs: true,
}
tfCtx, _ := terraform.NewContext(n.opts.ContextOpts)
waiter.update(tfCtx, moduletest.TearDown, nil)
plan, planDiags := tfCtx.Plan(module, state, planOpts)
diags = diags.Append(planDiags)
if diags.HasErrors() || plan.Errored {
return state, diags
}
if !plan.Complete {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Incomplete restore plan",
fmt.Sprintf("The restore plan for %s was reported as incomplete."+
" This means some of the cleanup operations were deferred due to unknown values, please check the rest of the output to see which resources could not be reverted.", run.Name)))
}
_, updated, applyDiags := apply(tfCtx, run, module, plan, moduletest.TearDown, variables, providers, waiter)
diags = diags.Append(applyDiags)
return updated, diags
}
func (n *NodeStateCleanup) destroy(ctx *EvalContext, file *configs.TestFile, run *configs.TestRun, module *configs.Config, state *states.State, waiter *operationWaiter) (*states.State, tfdiags.Diagnostics) {
log.Printf("[TRACE] TestFileRunner: called destroy for %s", run.Name)
variables, diags := GetVariables(ctx, run, module, false)
if diags.HasErrors() {
return state, diags
}
// we ignore the diagnostics from here, because we will have reported them
// during the initial execution of the run block and we would not have
// executed the run block if there were any errors.
providers, _, _ := getProviders(ctx, file, run, module)
// During the destroy operation, we don't add warnings from this operation.
// Anything that would have been reported here was already reported during
// the original plan, and a successful destroy operation is the only thing
// we care about.
setVariables, _, _ := FilterVariablesToModule(module, variables)
planOpts := &terraform.PlanOpts{
Mode: plans.DestroyMode,
SetVariables: setVariables,
Overrides: ctx.GetOverrides(run.Name),
ExternalProviders: providers,
SkipRefresh: true,
OverridePreventDestroy: true,
DeferralAllowed: ctx.deferralAllowed,
AllowRootEphemeralOutputs: true,
}
tfCtx, _ := terraform.NewContext(n.opts.ContextOpts)
waiter.update(tfCtx, moduletest.TearDown, nil)
plan, planDiags := tfCtx.Plan(module, state, planOpts)
diags = diags.Append(planDiags)
if diags.HasErrors() || plan.Errored {
return state, diags
}
if !plan.Complete {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
"Incomplete destroy plan",
fmt.Sprintf("The destroy plan for %s was reported as incomplete."+
" This means some of the cleanup operations were deferred due to unknown values, please check the rest of the output to see which resources could not be destroyed.", run.Name)))
}
_, updated, applyDiags := apply(tfCtx, run, module, plan, moduletest.TearDown, variables, providers, waiter)
diags = diags.Append(applyDiags)
return updated, diags
}
func emptyState(state *states.State) bool {
if state.Empty() {
return true
}
for _, module := range state.Modules {
for _, resource := range module.Resources {
if resource.Addr.Resource.Mode == addrs.ManagedResourceMode {
return false
}
}
}
return true
}