mirror of
https://github.com/hashicorp/terraform.git
synced 2026-03-21 18:10:30 -04:00
224 lines
8.8 KiB
Go
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
|
|
}
|