mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Merge pull request #25922 from hashicorp/jbardin/destroy-pruning
Fix node-pruning during destroy
This commit is contained in:
commit
058dff44f9
@ -11648,3 +11648,112 @@ output "output" {
|
|||||||
t.Fatalf("destroy apply errors: %s", diags.Err())
|
t.Fatalf("destroy apply errors: %s", diags.Err())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destroying properly requires pruning out all unneeded config nodes to
|
||||||
|
// prevent incorrect expansion evaluation.
|
||||||
|
func TestContext2Apply_destroyInterModuleExpansion(t *testing.T) {
|
||||||
|
m := testModuleInline(t, map[string]string{
|
||||||
|
"main.tf": `
|
||||||
|
data "test_data_source" "a" {
|
||||||
|
for_each = {
|
||||||
|
one = "thing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locals {
|
||||||
|
module_input = {
|
||||||
|
for k, v in data.test_data_source.a : k => v.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module "mod1" {
|
||||||
|
source = "./mod"
|
||||||
|
input = local.module_input
|
||||||
|
}
|
||||||
|
|
||||||
|
module "mod2" {
|
||||||
|
source = "./mod"
|
||||||
|
input = module.mod1.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "test_instance" "bar" {
|
||||||
|
for_each = module.mod2.outputs
|
||||||
|
}
|
||||||
|
|
||||||
|
output "module_output" {
|
||||||
|
value = module.mod2.outputs
|
||||||
|
}
|
||||||
|
output "test_instances" {
|
||||||
|
value = test_instance.bar
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
"mod/main.tf": `
|
||||||
|
variable "input" {
|
||||||
|
}
|
||||||
|
|
||||||
|
data "test_data_source" "foo" {
|
||||||
|
for_each = var.input
|
||||||
|
}
|
||||||
|
|
||||||
|
output "outputs" {
|
||||||
|
value = data.test_data_source.foo
|
||||||
|
}
|
||||||
|
`})
|
||||||
|
|
||||||
|
p := testProvider("test")
|
||||||
|
p.ApplyFn = testApplyFn
|
||||||
|
p.DiffFn = testDiffFn
|
||||||
|
p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
|
||||||
|
return providers.ReadDataSourceResponse{
|
||||||
|
State: cty.ObjectVal(map[string]cty.Value{
|
||||||
|
"id": cty.StringVal("data_source"),
|
||||||
|
"foo": cty.StringVal("output"),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := testContext2(t, &ContextOpts{
|
||||||
|
Config: m,
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, diags := ctx.Plan(); diags.HasErrors() {
|
||||||
|
t.Fatalf("plan errors: %s", diags.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
state, diags := ctx.Apply()
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("apply errors: %s", diags.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy := func() {
|
||||||
|
ctx = testContext2(t, &ContextOpts{
|
||||||
|
Config: m,
|
||||||
|
Providers: map[addrs.Provider]providers.Factory{
|
||||||
|
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
|
||||||
|
},
|
||||||
|
|
||||||
|
State: state,
|
||||||
|
Destroy: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, diags := ctx.Refresh(); diags.HasErrors() {
|
||||||
|
t.Fatalf("destroy plan errors: %s", diags.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, diags := ctx.Plan(); diags.HasErrors() {
|
||||||
|
t.Fatalf("destroy plan errors: %s", diags.Err())
|
||||||
|
}
|
||||||
|
|
||||||
|
state, diags = ctx.Apply()
|
||||||
|
if diags.HasErrors() {
|
||||||
|
t.Fatalf("destroy apply errors: %s", diags.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy()
|
||||||
|
// Destroying again from the empty state should not cause any errors either
|
||||||
|
destroy()
|
||||||
|
}
|
||||||
|
@ -2,7 +2,6 @@ package terraform
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/addrs"
|
"github.com/hashicorp/terraform/addrs"
|
||||||
"github.com/hashicorp/terraform/states"
|
"github.com/hashicorp/terraform/states"
|
||||||
@ -188,69 +187,9 @@ func (t *pruneUnusedNodesTransformer) Transform(g *Graph) error {
|
|||||||
// dependencies from child modules, freeing up nodes in the parent module
|
// dependencies from child modules, freeing up nodes in the parent module
|
||||||
// to also be removed.
|
// to also be removed.
|
||||||
|
|
||||||
// First collect the nodes into their respective modules based on
|
nodes := g.Vertices()
|
||||||
// configuration path.
|
|
||||||
moduleMap := make(map[string]pruneUnusedNodesMod)
|
|
||||||
for _, v := range g.Vertices() {
|
|
||||||
var path addrs.Module
|
|
||||||
switch v := v.(type) {
|
|
||||||
case GraphNodeModulePath:
|
|
||||||
path = v.ModulePath()
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m := moduleMap[path.String()]
|
|
||||||
m.addr = path
|
|
||||||
m.nodes = append(m.nodes, v)
|
|
||||||
|
|
||||||
moduleMap[path.String()] = m
|
for removed := true; removed; {
|
||||||
}
|
|
||||||
|
|
||||||
// now we need to restructure the modules so we can sort them
|
|
||||||
var modules []pruneUnusedNodesMod
|
|
||||||
|
|
||||||
for _, mod := range moduleMap {
|
|
||||||
modules = append(modules, mod)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort them by path length, longest first, so that we start with the
|
|
||||||
// deepest modules. The order of modules at the same tree level doesn't
|
|
||||||
// matter, we just need to ensure that child modules are processed before
|
|
||||||
// parent modules.
|
|
||||||
sort.Slice(modules, func(i, j int) bool {
|
|
||||||
return len(modules[i].addr) > len(modules[j].addr)
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, mod := range modules {
|
|
||||||
mod.removeUnused(g)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// pruneUnusedNodesMod is a container to hold the nodes that belong to a
|
|
||||||
// particular configuration module for the pruneUnusedNodesTransformer
|
|
||||||
type pruneUnusedNodesMod struct {
|
|
||||||
addr addrs.Module
|
|
||||||
nodes []dag.Vertex
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove any unused locals, variables, outputs and expanders. Since module
|
|
||||||
// closers can also lookup expansion info to detect orphaned instances, disable
|
|
||||||
// them if their associated expander is removed.
|
|
||||||
func (m *pruneUnusedNodesMod) removeUnused(g *Graph) {
|
|
||||||
// We modify the nodes slice during processing here.
|
|
||||||
// Make a copy so no one is surprised by this changing in the future.
|
|
||||||
nodes := make([]dag.Vertex, len(m.nodes))
|
|
||||||
copy(nodes, m.nodes)
|
|
||||||
|
|
||||||
// since we have no defined structure within the module, just cycle through
|
|
||||||
// the nodes in each module until there are no more removals
|
|
||||||
removed := true
|
|
||||||
for {
|
|
||||||
if !removed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
removed = false
|
removed = false
|
||||||
|
|
||||||
for i := 0; i < len(nodes); i++ {
|
for i := 0; i < len(nodes); i++ {
|
||||||
@ -307,4 +246,6 @@ func (m *pruneUnusedNodesMod) removeUnused(g *Graph) {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user