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