opentofu/internal/tofu/transform_destroy_edge_test.go
namgyalangmo cb2e9119aa
Update copyright notice (#1232)
Signed-off-by: namgyalangmo <75657887+namgyalangmo@users.noreply.github.com>
2024-02-08 09:48:59 +00:00

601 lines
20 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tofu
import (
"fmt"
"strings"
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/dag"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/states"
)
func TestDestroyEdgeTransformer_basic(t *testing.T) {
g := Graph{Path: addrs.RootModuleInstance}
g.Add(testDestroyNode("test_object.A"))
g.Add(testDestroyNode("test_object.B"))
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.A").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"A"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.B").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"B","test_string":"x"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
t.Fatal(err)
}
tf := &DestroyEdgeTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformDestroyEdgeBasicStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestDestroyEdgeTransformer_multi(t *testing.T) {
g := Graph{Path: addrs.RootModuleInstance}
g.Add(testDestroyNode("test_object.A"))
g.Add(testDestroyNode("test_object.B"))
g.Add(testDestroyNode("test_object.C"))
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.A").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"A"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.B").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"B","test_string":"x"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.C").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"C","test_string":"x"}`),
Dependencies: []addrs.ConfigResource{
mustConfigResourceAddr("test_object.A"),
mustConfigResourceAddr("test_object.B"),
},
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
t.Fatal(err)
}
tf := &DestroyEdgeTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformDestroyEdgeMultiStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestDestroyEdgeTransformer_selfRef(t *testing.T) {
g := Graph{Path: addrs.RootModuleInstance}
g.Add(testDestroyNode("test_object.A"))
tf := &DestroyEdgeTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformDestroyEdgeSelfRefStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestDestroyEdgeTransformer_module(t *testing.T) {
g := Graph{Path: addrs.RootModuleInstance}
g.Add(testDestroyNode("module.child.test_object.b"))
g.Add(testDestroyNode("test_object.a"))
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.a").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"a"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("module.child.test_object.b")},
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.b").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"b","test_string":"x"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
t.Fatal(err)
}
tf := &DestroyEdgeTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testTransformDestroyEdgeModuleStr)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestDestroyEdgeTransformer_moduleOnly(t *testing.T) {
g := Graph{Path: addrs.RootModuleInstance}
state := states.NewState()
for moduleIdx := 0; moduleIdx < 2; moduleIdx++ {
g.Add(testDestroyNode(fmt.Sprintf("module.child[%d].test_object.a", moduleIdx)))
g.Add(testDestroyNode(fmt.Sprintf("module.child[%d].test_object.b", moduleIdx)))
g.Add(testDestroyNode(fmt.Sprintf("module.child[%d].test_object.c", moduleIdx)))
child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.IntKey(moduleIdx)))
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.a").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"a"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.b").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"b","test_string":"x"}`),
Dependencies: []addrs.ConfigResource{
mustConfigResourceAddr("module.child.test_object.a"),
},
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
child.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.c").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"c","test_string":"x"}`),
Dependencies: []addrs.ConfigResource{
mustConfigResourceAddr("module.child.test_object.a"),
mustConfigResourceAddr("module.child.test_object.b"),
},
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
}
if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
t.Fatal(err)
}
tf := &DestroyEdgeTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
// The analyses done in the destroy edge transformer are between
// not-yet-expanded objects, which is conservative and so it will generate
// edges that aren't strictly necessary. As a special case we filter out
// any edges that are between resources instances that are in different
// instances of the same module, because those edges are never needed
// (one instance of a module cannot depend on another instance of the
// same module) and including them can, in complex cases, cause cycles due
// to unnecessary interactions between destroyed and created module
// instances in the same plan.
//
// Therefore below we expect to see the dependencies within each instance
// of module.child reflected, but we should not see any dependencies
// _between_ instances of module.child.
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(`
module.child[0].test_object.a (destroy)
module.child[0].test_object.b (destroy)
module.child[0].test_object.c (destroy)
module.child[0].test_object.b (destroy)
module.child[0].test_object.c (destroy)
module.child[0].test_object.c (destroy)
module.child[1].test_object.a (destroy)
module.child[1].test_object.b (destroy)
module.child[1].test_object.c (destroy)
module.child[1].test_object.b (destroy)
module.child[1].test_object.c (destroy)
module.child[1].test_object.c (destroy)
`)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestDestroyEdgeTransformer_destroyThenUpdate(t *testing.T) {
g := Graph{Path: addrs.RootModuleInstance}
g.Add(testUpdateNode("test_object.A"))
g.Add(testDestroyNode("test_object.B"))
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.A").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"A","test_string":"old"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.B").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"B","test_string":"x"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
t.Fatal(err)
}
tf := &DestroyEdgeTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
expected := strings.TrimSpace(`
test_object.A
test_object.B (destroy)
test_object.B (destroy)
`)
actual := strings.TrimSpace(g.String())
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestPruneUnusedNodesTransformer_rootModuleOutputValues(t *testing.T) {
// This is a kinda-weird test case covering the very narrow situation
// where a root module output value depends on a resource, where we
// need to make sure that the output value doesn't block pruning of
// the resource from the graph. This special case exists because although
// root module objects are "expanders", they in practice always expand
// to exactly one instance and so don't have the usual requirement of
// needing to stick around in order to support downstream expanders
// when there are e.g. nested expanding modules.
// In order to keep this test focused on the pruneUnusedNodesTransformer
// as much as possible we're using a minimal graph construction here which
// is just enough to get the nodes we need, but this does mean that this
// test might be invalidated by future changes to the apply graph builder,
// and so if something seems off here it might help to compare the
// following with the real apply graph transformer and verify whether
// this smaller construction is still realistic enough to be a valid test.
// It might be valid to change or remove this test to "make it work", as
// long as you verify that there is still _something_ upholding the
// invariant that a root module output value should not block a resource
// node from being pruned from the graph.
concreteResource := func(a *NodeAbstractResource) dag.Vertex {
return &nodeExpandApplyableResource{
NodeAbstractResource: a,
}
}
concreteResourceInstance := func(a *NodeAbstractResourceInstance) dag.Vertex {
return &NodeApplyableResourceInstance{
NodeAbstractResourceInstance: a,
}
}
resourceInstAddr := mustResourceInstanceAddr("test.a")
providerCfgAddr := addrs.AbsProviderConfig{
Module: addrs.RootModule,
Provider: addrs.MustParseProviderSourceString("foo/test"),
}
emptyObjDynamicVal, err := plans.NewDynamicValue(cty.EmptyObjectVal, cty.EmptyObject)
if err != nil {
t.Fatal(err)
}
nullObjDynamicVal, err := plans.NewDynamicValue(cty.NullVal(cty.EmptyObject), cty.EmptyObject)
if err != nil {
t.Fatal(err)
}
config := testModuleInline(t, map[string]string{
"main.tf": `
resource "test" "a" {
}
output "test" {
value = test.a.foo
}
`,
})
state := states.BuildState(func(s *states.SyncState) {
s.SetResourceInstanceCurrent(
resourceInstAddr,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{}`),
},
providerCfgAddr,
)
})
changes := plans.NewChanges()
changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{
Addr: resourceInstAddr,
PrevRunAddr: resourceInstAddr,
ProviderAddr: providerCfgAddr,
ChangeSrc: plans.ChangeSrc{
Action: plans.Delete,
Before: emptyObjDynamicVal,
After: nullObjDynamicVal,
},
})
builder := &BasicGraphBuilder{
Steps: []GraphTransformer{
&ConfigTransformer{
Concrete: concreteResource,
Config: config,
},
&OutputTransformer{
Config: config,
},
&DiffTransformer{
Concrete: concreteResourceInstance,
State: state,
Changes: changes,
},
&ReferenceTransformer{},
&AttachDependenciesTransformer{},
&pruneUnusedNodesTransformer{},
&CloseRootModuleTransformer{},
},
}
graph, diags := builder.Build(addrs.RootModuleInstance)
assertNoDiagnostics(t, diags)
// At this point, thanks to pruneUnusedNodesTransformer, we should still
// have the node for the output value, but the "test.a (expand)" node
// should've been pruned in recognition of the fact that we're performing
// a destroy and therefore we only need the "test.a (destroy)" node.
nodesByName := make(map[string]dag.Vertex)
nodesByResourceExpand := make(map[string]dag.Vertex)
for _, n := range graph.Vertices() {
name := dag.VertexName(n)
if _, exists := nodesByName[name]; exists {
t.Fatalf("multiple nodes have name %q", name)
}
nodesByName[name] = n
if exp, ok := n.(*nodeExpandApplyableResource); ok {
addr := exp.Addr
if _, exists := nodesByResourceExpand[addr.String()]; exists {
t.Fatalf("multiple nodes are expanders for %s", addr)
}
nodesByResourceExpand[addr.String()] = exp
}
}
// NOTE: The following is sensitive to the current name string formats we
// use for these particular node types. These names are not contractual
// so if this breaks in future it is fine to update these names to the new
// names as long as you verify first that the new names correspond to
// the same meaning as what we're assuming below.
if _, exists := nodesByName["test.a (destroy)"]; !exists {
t.Errorf("missing destroy node for resource instance test.a")
}
if _, exists := nodesByName["output.test (expand)"]; !exists {
t.Errorf("missing expand for output value 'test'")
}
// We _must not_ have any node that expands a resource.
if len(nodesByResourceExpand) != 0 {
t.Errorf("resource expand nodes remain the graph after transform; should've been pruned\n%s", spew.Sdump(nodesByResourceExpand))
}
}
// NoOp changes should not be participating in the destroy sequence
func TestDestroyEdgeTransformer_noOp(t *testing.T) {
g := Graph{Path: addrs.RootModuleInstance}
g.Add(testDestroyNode("test_object.A"))
g.Add(testUpdateNode("test_object.B"))
g.Add(testDestroyNode("test_object.C"))
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.A").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"A"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.B").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"B","test_string":"x"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")},
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.C").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"C","test_string":"x"}`),
Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A"),
mustConfigResourceAddr("test_object.B")},
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
t.Fatal(err)
}
tf := &DestroyEdgeTransformer{
// We only need a minimal object to indicate GraphNodeCreator change is
// a NoOp here.
Changes: &plans.Changes{
Resources: []*plans.ResourceInstanceChangeSrc{
{
Addr: mustResourceInstanceAddr("test_object.B"),
ChangeSrc: plans.ChangeSrc{Action: plans.NoOp},
},
},
},
}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
expected := strings.TrimSpace(`
test_object.A (destroy)
test_object.C (destroy)
test_object.B
test_object.C (destroy)`)
actual := strings.TrimSpace(g.String())
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func TestDestroyEdgeTransformer_dataDependsOn(t *testing.T) {
g := Graph{Path: addrs.RootModuleInstance}
addrA := mustResourceInstanceAddr("test_object.A")
instA := NewNodeAbstractResourceInstance(addrA)
a := &NodeDestroyResourceInstance{NodeAbstractResourceInstance: instA}
g.Add(a)
// B here represents a data sources, which is effectively an update during
// apply, but won't have dependencies stored in the state.
addrB := mustResourceInstanceAddr("test_object.B")
instB := NewNodeAbstractResourceInstance(addrB)
instB.Dependencies = append(instB.Dependencies, addrA.ConfigResource())
b := &NodeApplyableResourceInstance{NodeAbstractResourceInstance: instB}
g.Add(b)
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.A").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"id":"A"}`),
},
mustProviderConfig(`provider["registry.opentofu.org/hashicorp/test"]`),
)
if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil {
t.Fatal(err)
}
tf := &DestroyEdgeTransformer{}
if err := tf.Transform(&g); err != nil {
t.Fatalf("err: %s", err)
}
actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(`
test_object.A (destroy)
test_object.B
test_object.A (destroy)
`)
if actual != expected {
t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected)
}
}
func testDestroyNode(addrString string) GraphNodeDestroyer {
instAddr := mustResourceInstanceAddr(addrString)
inst := NewNodeAbstractResourceInstance(instAddr)
return &NodeDestroyResourceInstance{NodeAbstractResourceInstance: inst}
}
func testUpdateNode(addrString string) GraphNodeCreator {
instAddr := mustResourceInstanceAddr(addrString)
inst := NewNodeAbstractResourceInstance(instAddr)
return &NodeApplyableResourceInstance{NodeAbstractResourceInstance: inst}
}
const testTransformDestroyEdgeBasicStr = `
test_object.A (destroy)
test_object.B (destroy)
test_object.B (destroy)
`
const testTransformDestroyEdgeMultiStr = `
test_object.A (destroy)
test_object.B (destroy)
test_object.C (destroy)
test_object.B (destroy)
test_object.C (destroy)
test_object.C (destroy)
`
const testTransformDestroyEdgeSelfRefStr = `
test_object.A (destroy)
`
const testTransformDestroyEdgeModuleStr = `
module.child.test_object.b (destroy)
test_object.a (destroy)
test_object.a (destroy)
`