github.com/opentofu/opentofu@v1.7.1/internal/instances/expander_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package instances 7 8 import ( 9 "fmt" 10 "strings" 11 "testing" 12 13 "github.com/google/go-cmp/cmp" 14 "github.com/zclconf/go-cty/cty" 15 16 "github.com/opentofu/opentofu/internal/addrs" 17 ) 18 19 func TestExpander(t *testing.T) { 20 // Some module and resource addresses and values we'll use repeatedly below. 21 singleModuleAddr := addrs.ModuleCall{Name: "single"} 22 count2ModuleAddr := addrs.ModuleCall{Name: "count2"} 23 count0ModuleAddr := addrs.ModuleCall{Name: "count0"} 24 forEachModuleAddr := addrs.ModuleCall{Name: "for_each"} 25 singleResourceAddr := addrs.Resource{ 26 Mode: addrs.ManagedResourceMode, 27 Type: "test", 28 Name: "single", 29 } 30 count2ResourceAddr := addrs.Resource{ 31 Mode: addrs.ManagedResourceMode, 32 Type: "test", 33 Name: "count2", 34 } 35 count0ResourceAddr := addrs.Resource{ 36 Mode: addrs.ManagedResourceMode, 37 Type: "test", 38 Name: "count0", 39 } 40 forEachResourceAddr := addrs.Resource{ 41 Mode: addrs.ManagedResourceMode, 42 Type: "test", 43 Name: "for_each", 44 } 45 eachMap := map[string]cty.Value{ 46 "a": cty.NumberIntVal(1), 47 "b": cty.NumberIntVal(2), 48 } 49 50 // In normal use, Expander would be called in the context of a graph 51 // traversal to ensure that information is registered/requested in the 52 // correct sequence, but to keep this test self-contained we'll just 53 // manually write out the steps here. 54 // 55 // The steps below are assuming a configuration tree like the following: 56 // - root module 57 // - resource test.single with no count or for_each 58 // - resource test.count2 with count = 2 59 // - resource test.count0 with count = 0 60 // - resource test.for_each with for_each = { a = 1, b = 2 } 61 // - child module "single" with no count or for_each 62 // - resource test.single with no count or for_each 63 // - resource test.count2 with count = 2 64 // - child module "count2" with count = 2 65 // - resource test.single with no count or for_each 66 // - resource test.count2 with count = 2 67 // - child module "count2" with count = 2 68 // - resource test.count2 with count = 2 69 // - child module "count0" with count = 0 70 // - resource test.single with no count or for_each 71 // - child module for_each with for_each = { a = 1, b = 2 } 72 // - resource test.single with no count or for_each 73 // - resource test.count2 with count = 2 74 75 ex := NewExpander() 76 77 // We don't register the root module, because it's always implied to exist. 78 // 79 // Below we're going to use braces and indentation just to help visually 80 // reflect the tree structure from the tree in the above comment, in the 81 // hope that the following is easier to follow. 82 // 83 // The Expander API requires that we register containing modules before 84 // registering anything inside them, so we'll work through the above 85 // in a depth-first order in the registration steps that follow. 86 { 87 ex.SetResourceSingle(addrs.RootModuleInstance, singleResourceAddr) 88 ex.SetResourceCount(addrs.RootModuleInstance, count2ResourceAddr, 2) 89 ex.SetResourceCount(addrs.RootModuleInstance, count0ResourceAddr, 0) 90 ex.SetResourceForEach(addrs.RootModuleInstance, forEachResourceAddr, eachMap) 91 92 ex.SetModuleSingle(addrs.RootModuleInstance, singleModuleAddr) 93 { 94 // The single instance of the module 95 moduleInstanceAddr := addrs.RootModuleInstance.Child("single", addrs.NoKey) 96 ex.SetResourceSingle(moduleInstanceAddr, singleResourceAddr) 97 ex.SetResourceCount(moduleInstanceAddr, count2ResourceAddr, 2) 98 } 99 100 ex.SetModuleCount(addrs.RootModuleInstance, count2ModuleAddr, 2) 101 for i1 := 0; i1 < 2; i1++ { 102 moduleInstanceAddr := addrs.RootModuleInstance.Child("count2", addrs.IntKey(i1)) 103 ex.SetResourceSingle(moduleInstanceAddr, singleResourceAddr) 104 ex.SetResourceCount(moduleInstanceAddr, count2ResourceAddr, 2) 105 ex.SetModuleCount(moduleInstanceAddr, count2ModuleAddr, 2) 106 for i2 := 0; i2 < 2; i2++ { 107 moduleInstanceAddr := moduleInstanceAddr.Child("count2", addrs.IntKey(i2)) 108 ex.SetResourceCount(moduleInstanceAddr, count2ResourceAddr, 2) 109 } 110 } 111 112 ex.SetModuleCount(addrs.RootModuleInstance, count0ModuleAddr, 0) 113 { 114 // There are no instances of module "count0", so our nested module 115 // would never actually get registered here: the expansion node 116 // for the resource would see that its containing module has no 117 // instances and so do nothing. 118 } 119 120 ex.SetModuleForEach(addrs.RootModuleInstance, forEachModuleAddr, eachMap) 121 for k := range eachMap { 122 moduleInstanceAddr := addrs.RootModuleInstance.Child("for_each", addrs.StringKey(k)) 123 ex.SetResourceSingle(moduleInstanceAddr, singleResourceAddr) 124 ex.SetResourceCount(moduleInstanceAddr, count2ResourceAddr, 2) 125 } 126 } 127 128 t.Run("root module", func(t *testing.T) { 129 // Requesting expansion of the root module doesn't really mean anything 130 // since it's always a singleton, but for consistency it should work. 131 got := ex.ExpandModule(addrs.RootModule) 132 want := []addrs.ModuleInstance{addrs.RootModuleInstance} 133 if diff := cmp.Diff(want, got); diff != "" { 134 t.Errorf("wrong result\n%s", diff) 135 } 136 }) 137 t.Run("resource single", func(t *testing.T) { 138 got := ex.ExpandModuleResource( 139 addrs.RootModule, 140 singleResourceAddr, 141 ) 142 want := []addrs.AbsResourceInstance{ 143 mustAbsResourceInstanceAddr(`test.single`), 144 } 145 if diff := cmp.Diff(want, got); diff != "" { 146 t.Errorf("wrong result\n%s", diff) 147 } 148 }) 149 t.Run("resource count2", func(t *testing.T) { 150 got := ex.ExpandModuleResource( 151 addrs.RootModule, 152 count2ResourceAddr, 153 ) 154 want := []addrs.AbsResourceInstance{ 155 mustAbsResourceInstanceAddr(`test.count2[0]`), 156 mustAbsResourceInstanceAddr(`test.count2[1]`), 157 } 158 if diff := cmp.Diff(want, got); diff != "" { 159 t.Errorf("wrong result\n%s", diff) 160 } 161 }) 162 t.Run("resource count0", func(t *testing.T) { 163 got := ex.ExpandModuleResource( 164 addrs.RootModule, 165 count0ResourceAddr, 166 ) 167 want := []addrs.AbsResourceInstance(nil) 168 if diff := cmp.Diff(want, got); diff != "" { 169 t.Errorf("wrong result\n%s", diff) 170 } 171 }) 172 t.Run("resource for_each", func(t *testing.T) { 173 got := ex.ExpandModuleResource( 174 addrs.RootModule, 175 forEachResourceAddr, 176 ) 177 want := []addrs.AbsResourceInstance{ 178 mustAbsResourceInstanceAddr(`test.for_each["a"]`), 179 mustAbsResourceInstanceAddr(`test.for_each["b"]`), 180 } 181 if diff := cmp.Diff(want, got); diff != "" { 182 t.Errorf("wrong result\n%s", diff) 183 } 184 }) 185 t.Run("module single", func(t *testing.T) { 186 got := ex.ExpandModule(addrs.RootModule.Child("single")) 187 want := []addrs.ModuleInstance{ 188 mustModuleInstanceAddr(`module.single`), 189 } 190 if diff := cmp.Diff(want, got); diff != "" { 191 t.Errorf("wrong result\n%s", diff) 192 } 193 }) 194 t.Run("module single resource single", func(t *testing.T) { 195 got := ex.ExpandModuleResource( 196 mustModuleAddr("single"), 197 singleResourceAddr, 198 ) 199 want := []addrs.AbsResourceInstance{ 200 mustAbsResourceInstanceAddr("module.single.test.single"), 201 } 202 if diff := cmp.Diff(want, got); diff != "" { 203 t.Errorf("wrong result\n%s", diff) 204 } 205 }) 206 t.Run("module single resource count2", func(t *testing.T) { 207 // Two different ways of asking the same question, which should 208 // both produce the same result. 209 // First: nested expansion of all instances of the resource across 210 // all instances of the module, but it's a single-instance module 211 // so the first level is a singleton. 212 got1 := ex.ExpandModuleResource( 213 mustModuleAddr(`single`), 214 count2ResourceAddr, 215 ) 216 // Second: expansion of only instances belonging to a specific 217 // instance of the module, but again it's a single-instance module 218 // so there's only one to ask about. 219 got2 := ex.ExpandResource( 220 count2ResourceAddr.Absolute( 221 addrs.RootModuleInstance.Child("single", addrs.NoKey), 222 ), 223 ) 224 want := []addrs.AbsResourceInstance{ 225 mustAbsResourceInstanceAddr(`module.single.test.count2[0]`), 226 mustAbsResourceInstanceAddr(`module.single.test.count2[1]`), 227 } 228 if diff := cmp.Diff(want, got1); diff != "" { 229 t.Errorf("wrong ExpandModuleResource result\n%s", diff) 230 } 231 if diff := cmp.Diff(want, got2); diff != "" { 232 t.Errorf("wrong ExpandResource result\n%s", diff) 233 } 234 }) 235 t.Run("module single resource count2 with non-existing module instance", func(t *testing.T) { 236 got := ex.ExpandResource( 237 count2ResourceAddr.Absolute( 238 // Note: This is intentionally an invalid instance key, 239 // so we're asking about module.single[1].test.count2 240 // even though module.single doesn't have count set and 241 // therefore there is no module.single[1]. 242 addrs.RootModuleInstance.Child("single", addrs.IntKey(1)), 243 ), 244 ) 245 // If the containing module instance doesn't exist then it can't 246 // possibly have any resource instances inside it. 247 want := ([]addrs.AbsResourceInstance)(nil) 248 if diff := cmp.Diff(want, got); diff != "" { 249 t.Errorf("wrong result\n%s", diff) 250 } 251 }) 252 t.Run("module count2", func(t *testing.T) { 253 got := ex.ExpandModule(mustModuleAddr(`count2`)) 254 want := []addrs.ModuleInstance{ 255 mustModuleInstanceAddr(`module.count2[0]`), 256 mustModuleInstanceAddr(`module.count2[1]`), 257 } 258 if diff := cmp.Diff(want, got); diff != "" { 259 t.Errorf("wrong result\n%s", diff) 260 } 261 }) 262 t.Run("module count2 resource single", func(t *testing.T) { 263 got := ex.ExpandModuleResource( 264 mustModuleAddr(`count2`), 265 singleResourceAddr, 266 ) 267 want := []addrs.AbsResourceInstance{ 268 mustAbsResourceInstanceAddr(`module.count2[0].test.single`), 269 mustAbsResourceInstanceAddr(`module.count2[1].test.single`), 270 } 271 if diff := cmp.Diff(want, got); diff != "" { 272 t.Errorf("wrong result\n%s", diff) 273 } 274 }) 275 t.Run("module count2 resource count2", func(t *testing.T) { 276 got := ex.ExpandModuleResource( 277 mustModuleAddr(`count2`), 278 count2ResourceAddr, 279 ) 280 want := []addrs.AbsResourceInstance{ 281 mustAbsResourceInstanceAddr(`module.count2[0].test.count2[0]`), 282 mustAbsResourceInstanceAddr(`module.count2[0].test.count2[1]`), 283 mustAbsResourceInstanceAddr(`module.count2[1].test.count2[0]`), 284 mustAbsResourceInstanceAddr(`module.count2[1].test.count2[1]`), 285 } 286 if diff := cmp.Diff(want, got); diff != "" { 287 t.Errorf("wrong result\n%s", diff) 288 } 289 }) 290 t.Run("module count2 module count2", func(t *testing.T) { 291 got := ex.ExpandModule(mustModuleAddr(`count2.count2`)) 292 want := []addrs.ModuleInstance{ 293 mustModuleInstanceAddr(`module.count2[0].module.count2[0]`), 294 mustModuleInstanceAddr(`module.count2[0].module.count2[1]`), 295 mustModuleInstanceAddr(`module.count2[1].module.count2[0]`), 296 mustModuleInstanceAddr(`module.count2[1].module.count2[1]`), 297 } 298 if diff := cmp.Diff(want, got); diff != "" { 299 t.Errorf("wrong result\n%s", diff) 300 } 301 }) 302 t.Run("module count2 module count2 GetDeepestExistingModuleInstance", func(t *testing.T) { 303 t.Run("first step invalid", func(t *testing.T) { 304 got := ex.GetDeepestExistingModuleInstance(mustModuleInstanceAddr(`module.count2["nope"].module.count2[0]`)) 305 want := addrs.RootModuleInstance 306 if !want.Equal(got) { 307 t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) 308 } 309 }) 310 t.Run("second step invalid", func(t *testing.T) { 311 got := ex.GetDeepestExistingModuleInstance(mustModuleInstanceAddr(`module.count2[1].module.count2`)) 312 want := mustModuleInstanceAddr(`module.count2[1]`) 313 if !want.Equal(got) { 314 t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) 315 } 316 }) 317 t.Run("neither step valid", func(t *testing.T) { 318 got := ex.GetDeepestExistingModuleInstance(mustModuleInstanceAddr(`module.count2.module.count2["nope"]`)) 319 want := addrs.RootModuleInstance 320 if !want.Equal(got) { 321 t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) 322 } 323 }) 324 t.Run("both steps valid", func(t *testing.T) { 325 got := ex.GetDeepestExistingModuleInstance(mustModuleInstanceAddr(`module.count2[1].module.count2[0]`)) 326 want := mustModuleInstanceAddr(`module.count2[1].module.count2[0]`) 327 if !want.Equal(got) { 328 t.Errorf("wrong result\ngot: %s\nwant: %s", got, want) 329 } 330 }) 331 }) 332 t.Run("module count2 resource count2 resource count2", func(t *testing.T) { 333 got := ex.ExpandModuleResource( 334 mustModuleAddr(`count2.count2`), 335 count2ResourceAddr, 336 ) 337 want := []addrs.AbsResourceInstance{ 338 mustAbsResourceInstanceAddr(`module.count2[0].module.count2[0].test.count2[0]`), 339 mustAbsResourceInstanceAddr(`module.count2[0].module.count2[0].test.count2[1]`), 340 mustAbsResourceInstanceAddr(`module.count2[0].module.count2[1].test.count2[0]`), 341 mustAbsResourceInstanceAddr(`module.count2[0].module.count2[1].test.count2[1]`), 342 mustAbsResourceInstanceAddr(`module.count2[1].module.count2[0].test.count2[0]`), 343 mustAbsResourceInstanceAddr(`module.count2[1].module.count2[0].test.count2[1]`), 344 mustAbsResourceInstanceAddr(`module.count2[1].module.count2[1].test.count2[0]`), 345 mustAbsResourceInstanceAddr(`module.count2[1].module.count2[1].test.count2[1]`), 346 } 347 if diff := cmp.Diff(want, got); diff != "" { 348 t.Errorf("wrong result\n%s", diff) 349 } 350 }) 351 t.Run("module count2 resource count2 resource count2", func(t *testing.T) { 352 got := ex.ExpandResource( 353 count2ResourceAddr.Absolute(mustModuleInstanceAddr(`module.count2[0].module.count2[1]`)), 354 ) 355 want := []addrs.AbsResourceInstance{ 356 mustAbsResourceInstanceAddr(`module.count2[0].module.count2[1].test.count2[0]`), 357 mustAbsResourceInstanceAddr(`module.count2[0].module.count2[1].test.count2[1]`), 358 } 359 if diff := cmp.Diff(want, got); diff != "" { 360 t.Errorf("wrong result\n%s", diff) 361 } 362 }) 363 t.Run("module count0", func(t *testing.T) { 364 got := ex.ExpandModule(mustModuleAddr(`count0`)) 365 want := []addrs.ModuleInstance(nil) 366 if diff := cmp.Diff(want, got); diff != "" { 367 t.Errorf("wrong result\n%s", diff) 368 } 369 }) 370 t.Run("module count0 resource single", func(t *testing.T) { 371 got := ex.ExpandModuleResource( 372 mustModuleAddr(`count0`), 373 singleResourceAddr, 374 ) 375 // The containing module has zero instances, so therefore there 376 // are zero instances of this resource even though it doesn't have 377 // count = 0 set itself. 378 want := []addrs.AbsResourceInstance(nil) 379 if diff := cmp.Diff(want, got); diff != "" { 380 t.Errorf("wrong result\n%s", diff) 381 } 382 }) 383 t.Run("module for_each", func(t *testing.T) { 384 got := ex.ExpandModule(mustModuleAddr(`for_each`)) 385 want := []addrs.ModuleInstance{ 386 mustModuleInstanceAddr(`module.for_each["a"]`), 387 mustModuleInstanceAddr(`module.for_each["b"]`), 388 } 389 if diff := cmp.Diff(want, got); diff != "" { 390 t.Errorf("wrong result\n%s", diff) 391 } 392 }) 393 t.Run("module for_each resource single", func(t *testing.T) { 394 got := ex.ExpandModuleResource( 395 mustModuleAddr(`for_each`), 396 singleResourceAddr, 397 ) 398 want := []addrs.AbsResourceInstance{ 399 mustAbsResourceInstanceAddr(`module.for_each["a"].test.single`), 400 mustAbsResourceInstanceAddr(`module.for_each["b"].test.single`), 401 } 402 if diff := cmp.Diff(want, got); diff != "" { 403 t.Errorf("wrong result\n%s", diff) 404 } 405 }) 406 t.Run("module for_each resource count2", func(t *testing.T) { 407 got := ex.ExpandModuleResource( 408 mustModuleAddr(`for_each`), 409 count2ResourceAddr, 410 ) 411 want := []addrs.AbsResourceInstance{ 412 mustAbsResourceInstanceAddr(`module.for_each["a"].test.count2[0]`), 413 mustAbsResourceInstanceAddr(`module.for_each["a"].test.count2[1]`), 414 mustAbsResourceInstanceAddr(`module.for_each["b"].test.count2[0]`), 415 mustAbsResourceInstanceAddr(`module.for_each["b"].test.count2[1]`), 416 } 417 if diff := cmp.Diff(want, got); diff != "" { 418 t.Errorf("wrong result\n%s", diff) 419 } 420 }) 421 t.Run("module for_each resource count2", func(t *testing.T) { 422 got := ex.ExpandResource( 423 count2ResourceAddr.Absolute(mustModuleInstanceAddr(`module.for_each["a"]`)), 424 ) 425 want := []addrs.AbsResourceInstance{ 426 mustAbsResourceInstanceAddr(`module.for_each["a"].test.count2[0]`), 427 mustAbsResourceInstanceAddr(`module.for_each["a"].test.count2[1]`), 428 } 429 if diff := cmp.Diff(want, got); diff != "" { 430 t.Errorf("wrong result\n%s", diff) 431 } 432 }) 433 434 t.Run(`module.for_each["b"] repetitiondata`, func(t *testing.T) { 435 got := ex.GetModuleInstanceRepetitionData( 436 mustModuleInstanceAddr(`module.for_each["b"]`), 437 ) 438 want := RepetitionData{ 439 EachKey: cty.StringVal("b"), 440 EachValue: cty.NumberIntVal(2), 441 } 442 if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { 443 t.Errorf("wrong result\n%s", diff) 444 } 445 }) 446 t.Run(`module.count2[0].module.count2[1] repetitiondata`, func(t *testing.T) { 447 got := ex.GetModuleInstanceRepetitionData( 448 mustModuleInstanceAddr(`module.count2[0].module.count2[1]`), 449 ) 450 want := RepetitionData{ 451 CountIndex: cty.NumberIntVal(1), 452 } 453 if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { 454 t.Errorf("wrong result\n%s", diff) 455 } 456 }) 457 t.Run(`module.for_each["a"] repetitiondata`, func(t *testing.T) { 458 got := ex.GetModuleInstanceRepetitionData( 459 mustModuleInstanceAddr(`module.for_each["a"]`), 460 ) 461 want := RepetitionData{ 462 EachKey: cty.StringVal("a"), 463 EachValue: cty.NumberIntVal(1), 464 } 465 if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { 466 t.Errorf("wrong result\n%s", diff) 467 } 468 }) 469 470 t.Run(`test.for_each["a"] repetitiondata`, func(t *testing.T) { 471 got := ex.GetResourceInstanceRepetitionData( 472 mustAbsResourceInstanceAddr(`test.for_each["a"]`), 473 ) 474 want := RepetitionData{ 475 EachKey: cty.StringVal("a"), 476 EachValue: cty.NumberIntVal(1), 477 } 478 if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { 479 t.Errorf("wrong result\n%s", diff) 480 } 481 }) 482 t.Run(`module.for_each["a"].test.single repetitiondata`, func(t *testing.T) { 483 got := ex.GetResourceInstanceRepetitionData( 484 mustAbsResourceInstanceAddr(`module.for_each["a"].test.single`), 485 ) 486 want := RepetitionData{} 487 if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { 488 t.Errorf("wrong result\n%s", diff) 489 } 490 }) 491 t.Run(`module.for_each["a"].test.count2[1] repetitiondata`, func(t *testing.T) { 492 got := ex.GetResourceInstanceRepetitionData( 493 mustAbsResourceInstanceAddr(`module.for_each["a"].test.count2[1]`), 494 ) 495 want := RepetitionData{ 496 CountIndex: cty.NumberIntVal(1), 497 } 498 if diff := cmp.Diff(want, got, cmp.Comparer(valueEquals)); diff != "" { 499 t.Errorf("wrong result\n%s", diff) 500 } 501 }) 502 } 503 504 func mustAbsResourceInstanceAddr(str string) addrs.AbsResourceInstance { 505 addr, diags := addrs.ParseAbsResourceInstanceStr(str) 506 if diags.HasErrors() { 507 panic(fmt.Sprintf("invalid absolute resource instance address: %s", diags.Err())) 508 } 509 return addr 510 } 511 512 func mustModuleAddr(str string) addrs.Module { 513 if len(str) == 0 { 514 return addrs.RootModule 515 } 516 // We don't have a real parser for these because they don't appear in the 517 // language anywhere, but this interpretation mimics the format we 518 // produce from the String method on addrs.Module. 519 parts := strings.Split(str, ".") 520 return addrs.Module(parts) 521 } 522 523 func mustModuleInstanceAddr(str string) addrs.ModuleInstance { 524 if len(str) == 0 { 525 return addrs.RootModuleInstance 526 } 527 addr, diags := addrs.ParseModuleInstanceStr(str) 528 if diags.HasErrors() { 529 panic(fmt.Sprintf("invalid module instance address: %s", diags.Err())) 530 } 531 return addr 532 } 533 534 func valueEquals(a, b cty.Value) bool { 535 if a == cty.NilVal || b == cty.NilVal { 536 return a == b 537 } 538 return a.RawEquals(b) 539 }