github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/terraform/transform_destroy_edge_test.go (about) 1 package terraform 2 3 import ( 4 "fmt" 5 "strings" 6 "testing" 7 8 "github.com/davecgh/go-spew/spew" 9 "github.com/zclconf/go-cty/cty" 10 11 "github.com/hashicorp/terraform/internal/addrs" 12 "github.com/hashicorp/terraform/internal/dag" 13 "github.com/hashicorp/terraform/internal/plans" 14 "github.com/hashicorp/terraform/internal/states" 15 ) 16 17 func TestDestroyEdgeTransformer_basic(t *testing.T) { 18 g := Graph{Path: addrs.RootModuleInstance} 19 g.Add(testDestroyNode("test_object.A")) 20 g.Add(testDestroyNode("test_object.B")) 21 22 state := states.NewState() 23 root := state.EnsureModule(addrs.RootModuleInstance) 24 root.SetResourceInstanceCurrent( 25 mustResourceInstanceAddr("test_object.A").Resource, 26 &states.ResourceInstanceObjectSrc{ 27 Status: states.ObjectReady, 28 AttrsJSON: []byte(`{"id":"A"}`), 29 }, 30 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 31 ) 32 root.SetResourceInstanceCurrent( 33 mustResourceInstanceAddr("test_object.B").Resource, 34 &states.ResourceInstanceObjectSrc{ 35 Status: states.ObjectReady, 36 AttrsJSON: []byte(`{"id":"B","test_string":"x"}`), 37 Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")}, 38 }, 39 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 40 ) 41 if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil { 42 t.Fatal(err) 43 } 44 45 tf := &DestroyEdgeTransformer{} 46 if err := tf.Transform(&g); err != nil { 47 t.Fatalf("err: %s", err) 48 } 49 50 actual := strings.TrimSpace(g.String()) 51 expected := strings.TrimSpace(testTransformDestroyEdgeBasicStr) 52 if actual != expected { 53 t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) 54 } 55 } 56 57 func TestDestroyEdgeTransformer_multi(t *testing.T) { 58 g := Graph{Path: addrs.RootModuleInstance} 59 g.Add(testDestroyNode("test_object.A")) 60 g.Add(testDestroyNode("test_object.B")) 61 g.Add(testDestroyNode("test_object.C")) 62 63 state := states.NewState() 64 root := state.EnsureModule(addrs.RootModuleInstance) 65 root.SetResourceInstanceCurrent( 66 mustResourceInstanceAddr("test_object.A").Resource, 67 &states.ResourceInstanceObjectSrc{ 68 Status: states.ObjectReady, 69 AttrsJSON: []byte(`{"id":"A"}`), 70 }, 71 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 72 ) 73 root.SetResourceInstanceCurrent( 74 mustResourceInstanceAddr("test_object.B").Resource, 75 &states.ResourceInstanceObjectSrc{ 76 Status: states.ObjectReady, 77 AttrsJSON: []byte(`{"id":"B","test_string":"x"}`), 78 Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")}, 79 }, 80 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 81 ) 82 root.SetResourceInstanceCurrent( 83 mustResourceInstanceAddr("test_object.C").Resource, 84 &states.ResourceInstanceObjectSrc{ 85 Status: states.ObjectReady, 86 AttrsJSON: []byte(`{"id":"C","test_string":"x"}`), 87 Dependencies: []addrs.ConfigResource{ 88 mustConfigResourceAddr("test_object.A"), 89 mustConfigResourceAddr("test_object.B"), 90 }, 91 }, 92 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 93 ) 94 95 if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil { 96 t.Fatal(err) 97 } 98 99 tf := &DestroyEdgeTransformer{} 100 if err := tf.Transform(&g); err != nil { 101 t.Fatalf("err: %s", err) 102 } 103 104 actual := strings.TrimSpace(g.String()) 105 expected := strings.TrimSpace(testTransformDestroyEdgeMultiStr) 106 if actual != expected { 107 t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) 108 } 109 } 110 111 func TestDestroyEdgeTransformer_selfRef(t *testing.T) { 112 g := Graph{Path: addrs.RootModuleInstance} 113 g.Add(testDestroyNode("test_object.A")) 114 tf := &DestroyEdgeTransformer{} 115 if err := tf.Transform(&g); err != nil { 116 t.Fatalf("err: %s", err) 117 } 118 119 actual := strings.TrimSpace(g.String()) 120 expected := strings.TrimSpace(testTransformDestroyEdgeSelfRefStr) 121 if actual != expected { 122 t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) 123 } 124 } 125 126 func TestDestroyEdgeTransformer_module(t *testing.T) { 127 g := Graph{Path: addrs.RootModuleInstance} 128 g.Add(testDestroyNode("module.child.test_object.b")) 129 g.Add(testDestroyNode("test_object.a")) 130 state := states.NewState() 131 root := state.EnsureModule(addrs.RootModuleInstance) 132 child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) 133 root.SetResourceInstanceCurrent( 134 mustResourceInstanceAddr("test_object.a").Resource, 135 &states.ResourceInstanceObjectSrc{ 136 Status: states.ObjectReady, 137 AttrsJSON: []byte(`{"id":"a"}`), 138 Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("module.child.test_object.b")}, 139 }, 140 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 141 ) 142 child.SetResourceInstanceCurrent( 143 mustResourceInstanceAddr("test_object.b").Resource, 144 &states.ResourceInstanceObjectSrc{ 145 Status: states.ObjectReady, 146 AttrsJSON: []byte(`{"id":"b","test_string":"x"}`), 147 }, 148 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 149 ) 150 151 if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil { 152 t.Fatal(err) 153 } 154 155 tf := &DestroyEdgeTransformer{} 156 if err := tf.Transform(&g); err != nil { 157 t.Fatalf("err: %s", err) 158 } 159 160 actual := strings.TrimSpace(g.String()) 161 expected := strings.TrimSpace(testTransformDestroyEdgeModuleStr) 162 if actual != expected { 163 t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) 164 } 165 } 166 167 func TestDestroyEdgeTransformer_moduleOnly(t *testing.T) { 168 g := Graph{Path: addrs.RootModuleInstance} 169 170 state := states.NewState() 171 for moduleIdx := 0; moduleIdx < 2; moduleIdx++ { 172 g.Add(testDestroyNode(fmt.Sprintf("module.child[%d].test_object.a", moduleIdx))) 173 g.Add(testDestroyNode(fmt.Sprintf("module.child[%d].test_object.b", moduleIdx))) 174 g.Add(testDestroyNode(fmt.Sprintf("module.child[%d].test_object.c", moduleIdx))) 175 176 child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.IntKey(moduleIdx))) 177 child.SetResourceInstanceCurrent( 178 mustResourceInstanceAddr("test_object.a").Resource, 179 &states.ResourceInstanceObjectSrc{ 180 Status: states.ObjectReady, 181 AttrsJSON: []byte(`{"id":"a"}`), 182 }, 183 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 184 ) 185 child.SetResourceInstanceCurrent( 186 mustResourceInstanceAddr("test_object.b").Resource, 187 &states.ResourceInstanceObjectSrc{ 188 Status: states.ObjectReady, 189 AttrsJSON: []byte(`{"id":"b","test_string":"x"}`), 190 Dependencies: []addrs.ConfigResource{ 191 mustConfigResourceAddr("module.child.test_object.a"), 192 }, 193 }, 194 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 195 ) 196 child.SetResourceInstanceCurrent( 197 mustResourceInstanceAddr("test_object.c").Resource, 198 &states.ResourceInstanceObjectSrc{ 199 Status: states.ObjectReady, 200 AttrsJSON: []byte(`{"id":"c","test_string":"x"}`), 201 Dependencies: []addrs.ConfigResource{ 202 mustConfigResourceAddr("module.child.test_object.a"), 203 mustConfigResourceAddr("module.child.test_object.b"), 204 }, 205 }, 206 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 207 ) 208 } 209 210 if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil { 211 t.Fatal(err) 212 } 213 214 tf := &DestroyEdgeTransformer{} 215 if err := tf.Transform(&g); err != nil { 216 t.Fatalf("err: %s", err) 217 } 218 219 // The analyses done in the destroy edge transformer are between 220 // not-yet-expanded objects, which is conservative and so it will generate 221 // edges that aren't strictly necessary. As a special case we filter out 222 // any edges that are between resources instances that are in different 223 // instances of the same module, because those edges are never needed 224 // (one instance of a module cannot depend on another instance of the 225 // same module) and including them can, in complex cases, cause cycles due 226 // to unnecessary interactions between destroyed and created module 227 // instances in the same plan. 228 // 229 // Therefore below we expect to see the dependencies within each instance 230 // of module.child reflected, but we should not see any dependencies 231 // _between_ instances of module.child. 232 233 actual := strings.TrimSpace(g.String()) 234 expected := strings.TrimSpace(` 235 module.child[0].test_object.a (destroy) 236 module.child[0].test_object.b (destroy) 237 module.child[0].test_object.c (destroy) 238 module.child[0].test_object.b (destroy) 239 module.child[0].test_object.c (destroy) 240 module.child[0].test_object.c (destroy) 241 module.child[1].test_object.a (destroy) 242 module.child[1].test_object.b (destroy) 243 module.child[1].test_object.c (destroy) 244 module.child[1].test_object.b (destroy) 245 module.child[1].test_object.c (destroy) 246 module.child[1].test_object.c (destroy) 247 `) 248 if actual != expected { 249 t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) 250 } 251 } 252 253 func TestDestroyEdgeTransformer_destroyThenUpdate(t *testing.T) { 254 g := Graph{Path: addrs.RootModuleInstance} 255 g.Add(testUpdateNode("test_object.A")) 256 g.Add(testDestroyNode("test_object.B")) 257 258 state := states.NewState() 259 root := state.EnsureModule(addrs.RootModuleInstance) 260 root.SetResourceInstanceCurrent( 261 mustResourceInstanceAddr("test_object.A").Resource, 262 &states.ResourceInstanceObjectSrc{ 263 Status: states.ObjectReady, 264 AttrsJSON: []byte(`{"id":"A","test_string":"old"}`), 265 }, 266 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 267 ) 268 root.SetResourceInstanceCurrent( 269 mustResourceInstanceAddr("test_object.B").Resource, 270 &states.ResourceInstanceObjectSrc{ 271 Status: states.ObjectReady, 272 AttrsJSON: []byte(`{"id":"B","test_string":"x"}`), 273 Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")}, 274 }, 275 mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), 276 ) 277 278 if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil { 279 t.Fatal(err) 280 } 281 282 tf := &DestroyEdgeTransformer{} 283 if err := tf.Transform(&g); err != nil { 284 t.Fatalf("err: %s", err) 285 } 286 287 expected := strings.TrimSpace(` 288 test_object.A 289 test_object.B (destroy) 290 test_object.B (destroy) 291 `) 292 actual := strings.TrimSpace(g.String()) 293 294 if actual != expected { 295 t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) 296 } 297 } 298 299 func TestPruneUnusedNodesTransformer_rootModuleOutputValues(t *testing.T) { 300 // This is a kinda-weird test case covering the very narrow situation 301 // where a root module output value depends on a resource, where we 302 // need to make sure that the output value doesn't block pruning of 303 // the resource from the graph. This special case exists because although 304 // root module objects are "expanders", they in practice always expand 305 // to exactly one instance and so don't have the usual requirement of 306 // needing to stick around in order to support downstream expanders 307 // when there are e.g. nested expanding modules. 308 309 // In order to keep this test focused on the pruneUnusedNodesTransformer 310 // as much as possible we're using a minimal graph construction here which 311 // is just enough to get the nodes we need, but this does mean that this 312 // test might be invalidated by future changes to the apply graph builder, 313 // and so if something seems off here it might help to compare the 314 // following with the real apply graph transformer and verify whether 315 // this smaller construction is still realistic enough to be a valid test. 316 // It might be valid to change or remove this test to "make it work", as 317 // long as you verify that there is still _something_ upholding the 318 // invariant that a root module output value should not block a resource 319 // node from being pruned from the graph. 320 321 concreteResource := func(a *NodeAbstractResource) dag.Vertex { 322 return &nodeExpandApplyableResource{ 323 NodeAbstractResource: a, 324 } 325 } 326 327 concreteResourceInstance := func(a *NodeAbstractResourceInstance) dag.Vertex { 328 return &NodeApplyableResourceInstance{ 329 NodeAbstractResourceInstance: a, 330 } 331 } 332 333 resourceInstAddr := mustResourceInstanceAddr("test.a") 334 providerCfgAddr := addrs.AbsProviderConfig{ 335 Module: addrs.RootModule, 336 Provider: addrs.MustParseProviderSourceString("foo/test"), 337 } 338 emptyObjDynamicVal, err := plans.NewDynamicValue(cty.EmptyObjectVal, cty.EmptyObject) 339 if err != nil { 340 t.Fatal(err) 341 } 342 nullObjDynamicVal, err := plans.NewDynamicValue(cty.NullVal(cty.EmptyObject), cty.EmptyObject) 343 if err != nil { 344 t.Fatal(err) 345 } 346 347 config := testModuleInline(t, map[string]string{ 348 "main.tf": ` 349 resource "test" "a" { 350 } 351 352 output "test" { 353 value = test.a.foo 354 } 355 `, 356 }) 357 state := states.BuildState(func(s *states.SyncState) { 358 s.SetResourceInstanceCurrent( 359 resourceInstAddr, 360 &states.ResourceInstanceObjectSrc{ 361 Status: states.ObjectReady, 362 AttrsJSON: []byte(`{}`), 363 }, 364 providerCfgAddr, 365 ) 366 }) 367 changes := plans.NewChanges() 368 changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ 369 Addr: resourceInstAddr, 370 PrevRunAddr: resourceInstAddr, 371 ProviderAddr: providerCfgAddr, 372 ChangeSrc: plans.ChangeSrc{ 373 Action: plans.Delete, 374 Before: emptyObjDynamicVal, 375 After: nullObjDynamicVal, 376 }, 377 }) 378 379 builder := &BasicGraphBuilder{ 380 Steps: []GraphTransformer{ 381 &ConfigTransformer{ 382 Concrete: concreteResource, 383 Config: config, 384 }, 385 &OutputTransformer{ 386 Config: config, 387 Changes: changes, 388 }, 389 &DiffTransformer{ 390 Concrete: concreteResourceInstance, 391 State: state, 392 Changes: changes, 393 }, 394 &ReferenceTransformer{}, 395 &AttachDependenciesTransformer{}, 396 &pruneUnusedNodesTransformer{}, 397 &CloseRootModuleTransformer{}, 398 }, 399 } 400 graph, diags := builder.Build(addrs.RootModuleInstance) 401 assertNoDiagnostics(t, diags) 402 403 // At this point, thanks to pruneUnusedNodesTransformer, we should still 404 // have the node for the output value, but the "test.a (expand)" node 405 // should've been pruned in recognition of the fact that we're performing 406 // a destroy and therefore we only need the "test.a (destroy)" node. 407 408 nodesByName := make(map[string]dag.Vertex) 409 nodesByResourceExpand := make(map[string]dag.Vertex) 410 for _, n := range graph.Vertices() { 411 name := dag.VertexName(n) 412 if _, exists := nodesByName[name]; exists { 413 t.Fatalf("multiple nodes have name %q", name) 414 } 415 nodesByName[name] = n 416 417 if exp, ok := n.(*nodeExpandApplyableResource); ok { 418 addr := exp.Addr 419 if _, exists := nodesByResourceExpand[addr.String()]; exists { 420 t.Fatalf("multiple nodes are expanders for %s", addr) 421 } 422 nodesByResourceExpand[addr.String()] = exp 423 } 424 } 425 426 // NOTE: The following is sensitive to the current name string formats we 427 // use for these particular node types. These names are not contractual 428 // so if this breaks in future it is fine to update these names to the new 429 // names as long as you verify first that the new names correspond to 430 // the same meaning as what we're assuming below. 431 if _, exists := nodesByName["test.a (destroy)"]; !exists { 432 t.Errorf("missing destroy node for resource instance test.a") 433 } 434 if _, exists := nodesByName["output.test (expand)"]; !exists { 435 t.Errorf("missing expand for output value 'test'") 436 } 437 438 // We _must not_ have any node that expands a resource. 439 if len(nodesByResourceExpand) != 0 { 440 t.Errorf("resource expand nodes remain the graph after transform; should've been pruned\n%s", spew.Sdump(nodesByResourceExpand)) 441 } 442 } 443 444 func testDestroyNode(addrString string) GraphNodeDestroyer { 445 instAddr := mustResourceInstanceAddr(addrString) 446 inst := NewNodeAbstractResourceInstance(instAddr) 447 return &NodeDestroyResourceInstance{NodeAbstractResourceInstance: inst} 448 } 449 450 func testUpdateNode(addrString string) GraphNodeCreator { 451 instAddr := mustResourceInstanceAddr(addrString) 452 inst := NewNodeAbstractResourceInstance(instAddr) 453 return &NodeApplyableResourceInstance{NodeAbstractResourceInstance: inst} 454 } 455 456 const testTransformDestroyEdgeBasicStr = ` 457 test_object.A (destroy) 458 test_object.B (destroy) 459 test_object.B (destroy) 460 ` 461 462 const testTransformDestroyEdgeMultiStr = ` 463 test_object.A (destroy) 464 test_object.B (destroy) 465 test_object.C (destroy) 466 test_object.B (destroy) 467 test_object.C (destroy) 468 test_object.C (destroy) 469 ` 470 471 const testTransformDestroyEdgeSelfRefStr = ` 472 test_object.A (destroy) 473 ` 474 475 const testTransformDestroyEdgeModuleStr = ` 476 module.child.test_object.b (destroy) 477 test_object.a (destroy) 478 test_object.a (destroy) 479 `