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