diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index 01cf74fe1b54..e6355e72da98 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -3518,3 +3518,44 @@ resource "test_object" "b" { t.Fatalf("no cycle error found:\n got: %s\n", msg) } } + +// plan a destroy with no state where configuration could fail to evaluate +// expansion indexes. +func TestContext2Plan_emptyDestroy(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +locals { + enable = true + value = local.enable ? module.example[0].out : null +} + +module "example" { + count = local.enable ? 1 : 0 + source = "./example" +} +`, + "example/main.tf": ` +resource "test_resource" "x" { +} + +output "out" { + value = test_resource.x +} +`, + }) + + p := testProvider("test") + state := states.NewState() + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + }) + + assertNoErrors(t, diags) +} diff --git a/internal/terraform/graph_builder_plan.go b/internal/terraform/graph_builder_plan.go index 2f0a9f2d87d0..d65bab26def0 100644 --- a/internal/terraform/graph_builder_plan.go +++ b/internal/terraform/graph_builder_plan.go @@ -170,6 +170,10 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { // TargetsTransformer can determine which nodes to keep in the graph. &DestroyEdgeTransformer{}, + &pruneUnusedNodesTransformer{ + skip: b.Operation != walkPlanDestroy, + }, + // Target &TargetsTransformer{Targets: b.Targets}, diff --git a/internal/terraform/node_resource_plan.go b/internal/terraform/node_resource_plan.go index c99654e3b080..7db381f90db8 100644 --- a/internal/terraform/node_resource_plan.go +++ b/internal/terraform/node_resource_plan.go @@ -53,12 +53,16 @@ var ( _ GraphNodeAttachResourceConfig = (*nodeExpandPlannableResource)(nil) _ GraphNodeAttachDependencies = (*nodeExpandPlannableResource)(nil) _ GraphNodeTargetable = (*nodeExpandPlannableResource)(nil) + _ graphNodeExpandsInstances = (*nodeExpandPlannableResource)(nil) ) func (n *nodeExpandPlannableResource) Name() string { return n.NodeAbstractResource.Name() + " (expand)" } +func (n *nodeExpandPlannableResource) expandsInstances() { +} + // GraphNodeAttachDependencies func (n *nodeExpandPlannableResource) AttachDependencies(deps []addrs.ConfigResource) { n.dependencies = deps diff --git a/internal/terraform/transform_destroy_edge.go b/internal/terraform/transform_destroy_edge.go index bed62009a039..a1a60cb63146 100644 --- a/internal/terraform/transform_destroy_edge.go +++ b/internal/terraform/transform_destroy_edge.go @@ -227,9 +227,18 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error { // closers also need to disable their use of expansion if the module itself is // no longer present. type pruneUnusedNodesTransformer struct { + // The plan graph builder will skip this transformer except during a full + // destroy. Planing normally involves all nodes, but during a destroy plan + // we may need to prune things which are in the configuration but do not + // exist in state to evaluate. + skip bool } func (t *pruneUnusedNodesTransformer) Transform(g *Graph) error { + if t.skip { + return nil + } + // We need a reverse depth first walk of modules, processing them in order // from the leaf modules to the root. This allows us to remove unneeded // dependencies from child modules, freeing up nodes in the parent module