github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/legacy/terraform/resource_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package terraform 5 6 import ( 7 "fmt" 8 "reflect" 9 "testing" 10 11 "github.com/terramate-io/tf/configs/configschema" 12 "github.com/zclconf/go-cty/cty" 13 14 "github.com/mitchellh/reflectwalk" 15 "github.com/terramate-io/tf/configs/hcl2shim" 16 ) 17 18 func TestResourceConfigGet(t *testing.T) { 19 fooStringSchema := &configschema.Block{ 20 Attributes: map[string]*configschema.Attribute{ 21 "foo": {Type: cty.String, Optional: true}, 22 }, 23 } 24 fooListSchema := &configschema.Block{ 25 Attributes: map[string]*configschema.Attribute{ 26 "foo": {Type: cty.List(cty.Number), Optional: true}, 27 }, 28 } 29 30 cases := []struct { 31 Config cty.Value 32 Schema *configschema.Block 33 Key string 34 Value interface{} 35 }{ 36 { 37 Config: cty.ObjectVal(map[string]cty.Value{ 38 "foo": cty.StringVal("bar"), 39 }), 40 Schema: fooStringSchema, 41 Key: "foo", 42 Value: "bar", 43 }, 44 45 { 46 Config: cty.ObjectVal(map[string]cty.Value{ 47 "foo": cty.UnknownVal(cty.String), 48 }), 49 Schema: fooStringSchema, 50 Key: "foo", 51 Value: hcl2shim.UnknownVariableValue, 52 }, 53 54 { 55 Config: cty.ObjectVal(map[string]cty.Value{ 56 "foo": cty.ListVal([]cty.Value{ 57 cty.NumberIntVal(1), 58 cty.NumberIntVal(2), 59 cty.NumberIntVal(5), 60 }), 61 }), 62 Schema: fooListSchema, 63 Key: "foo.0", 64 Value: 1, 65 }, 66 67 { 68 Config: cty.ObjectVal(map[string]cty.Value{ 69 "foo": cty.ListVal([]cty.Value{ 70 cty.NumberIntVal(1), 71 cty.NumberIntVal(2), 72 cty.NumberIntVal(5), 73 }), 74 }), 75 Schema: fooListSchema, 76 Key: "foo.5", 77 Value: nil, 78 }, 79 80 { 81 Config: cty.ObjectVal(map[string]cty.Value{ 82 "foo": cty.ListVal([]cty.Value{ 83 cty.NumberIntVal(1), 84 cty.NumberIntVal(2), 85 cty.NumberIntVal(5), 86 }), 87 }), 88 Schema: fooListSchema, 89 Key: "foo.-1", 90 Value: nil, 91 }, 92 93 // get from map 94 { 95 Config: cty.ObjectVal(map[string]cty.Value{ 96 "mapname": cty.ListVal([]cty.Value{ 97 cty.MapVal(map[string]cty.Value{ 98 "key": cty.NumberIntVal(1), 99 }), 100 }), 101 }), 102 Schema: &configschema.Block{ 103 Attributes: map[string]*configschema.Attribute{ 104 "mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true}, 105 }, 106 }, 107 Key: "mapname.0.key", 108 Value: 1, 109 }, 110 111 // get from map with dot in key 112 { 113 Config: cty.ObjectVal(map[string]cty.Value{ 114 "mapname": cty.ListVal([]cty.Value{ 115 cty.MapVal(map[string]cty.Value{ 116 "key.name": cty.NumberIntVal(1), 117 }), 118 }), 119 }), 120 Schema: &configschema.Block{ 121 Attributes: map[string]*configschema.Attribute{ 122 "mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true}, 123 }, 124 }, 125 Key: "mapname.0.key.name", 126 Value: 1, 127 }, 128 129 // get from map with overlapping key names 130 { 131 Config: cty.ObjectVal(map[string]cty.Value{ 132 "mapname": cty.ListVal([]cty.Value{ 133 cty.MapVal(map[string]cty.Value{ 134 "key.name": cty.NumberIntVal(1), 135 "key.name.2": cty.NumberIntVal(2), 136 }), 137 }), 138 }), 139 Schema: &configschema.Block{ 140 Attributes: map[string]*configschema.Attribute{ 141 "mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true}, 142 }, 143 }, 144 Key: "mapname.0.key.name.2", 145 Value: 2, 146 }, 147 { 148 Config: cty.ObjectVal(map[string]cty.Value{ 149 "mapname": cty.ListVal([]cty.Value{ 150 cty.MapVal(map[string]cty.Value{ 151 "key.name": cty.NumberIntVal(1), 152 "key.name.foo": cty.NumberIntVal(2), 153 }), 154 }), 155 }), 156 Schema: &configschema.Block{ 157 Attributes: map[string]*configschema.Attribute{ 158 "mapname": {Type: cty.List(cty.Map(cty.Number)), Optional: true}, 159 }, 160 }, 161 Key: "mapname.0.key.name", 162 Value: 1, 163 }, 164 { 165 Config: cty.ObjectVal(map[string]cty.Value{ 166 "mapname": cty.ListVal([]cty.Value{ 167 cty.MapVal(map[string]cty.Value{ 168 "listkey": cty.ListVal([]cty.Value{ 169 cty.MapVal(map[string]cty.Value{ 170 "key": cty.NumberIntVal(3), 171 }), 172 }), 173 }), 174 }), 175 }), 176 Schema: &configschema.Block{ 177 Attributes: map[string]*configschema.Attribute{ 178 "mapname": {Type: cty.List(cty.Map(cty.List(cty.Map(cty.Number)))), Optional: true}, 179 }, 180 }, 181 Key: "mapname.0.listkey.0.key", 182 Value: 3, 183 }, 184 } 185 186 for i, tc := range cases { 187 rc := NewResourceConfigShimmed(tc.Config, tc.Schema) 188 189 // Test getting a key 190 t.Run(fmt.Sprintf("get-%d", i), func(t *testing.T) { 191 v, ok := rc.Get(tc.Key) 192 if ok && v == nil { 193 t.Fatal("(nil, true) returned from Get") 194 } 195 196 if !reflect.DeepEqual(v, tc.Value) { 197 t.Fatalf("%d bad: %#v", i, v) 198 } 199 }) 200 201 // Test copying and equality 202 t.Run(fmt.Sprintf("copy-and-equal-%d", i), func(t *testing.T) { 203 copy := rc.DeepCopy() 204 if !reflect.DeepEqual(copy, rc) { 205 t.Fatalf("bad:\n\n%#v\n\n%#v", copy, rc) 206 } 207 208 if !copy.Equal(rc) { 209 t.Fatalf("copy != rc:\n\n%#v\n\n%#v", copy, rc) 210 } 211 if !rc.Equal(copy) { 212 t.Fatalf("rc != copy:\n\n%#v\n\n%#v", copy, rc) 213 } 214 }) 215 } 216 } 217 218 func TestResourceConfigDeepCopy_nil(t *testing.T) { 219 var nilRc *ResourceConfig 220 actual := nilRc.DeepCopy() 221 if actual != nil { 222 t.Fatalf("bad: %#v", actual) 223 } 224 } 225 226 func TestResourceConfigDeepCopy_nilComputed(t *testing.T) { 227 rc := &ResourceConfig{} 228 actual := rc.DeepCopy() 229 if actual.ComputedKeys != nil { 230 t.Fatalf("bad: %#v", actual) 231 } 232 } 233 234 func TestResourceConfigEqual_nil(t *testing.T) { 235 var nilRc *ResourceConfig 236 notNil := NewResourceConfigShimmed(cty.EmptyObjectVal, &configschema.Block{}) 237 238 if nilRc.Equal(notNil) { 239 t.Fatal("should not be equal") 240 } 241 242 if notNil.Equal(nilRc) { 243 t.Fatal("should not be equal") 244 } 245 } 246 247 func TestResourceConfigEqual_computedKeyOrder(t *testing.T) { 248 v := cty.ObjectVal(map[string]cty.Value{ 249 "foo": cty.UnknownVal(cty.String), 250 }) 251 schema := &configschema.Block{ 252 Attributes: map[string]*configschema.Attribute{ 253 "foo": {Type: cty.String, Optional: true}, 254 }, 255 } 256 rc := NewResourceConfigShimmed(v, schema) 257 rc2 := NewResourceConfigShimmed(v, schema) 258 259 // Set the computed keys manually to force ordering to differ 260 rc.ComputedKeys = []string{"foo", "bar"} 261 rc2.ComputedKeys = []string{"bar", "foo"} 262 263 if !rc.Equal(rc2) { 264 t.Fatal("should be equal") 265 } 266 } 267 268 func TestUnknownCheckWalker(t *testing.T) { 269 cases := []struct { 270 Name string 271 Input interface{} 272 Result bool 273 }{ 274 { 275 "primitive", 276 42, 277 false, 278 }, 279 280 { 281 "primitive computed", 282 hcl2shim.UnknownVariableValue, 283 true, 284 }, 285 286 { 287 "list", 288 []interface{}{"foo", hcl2shim.UnknownVariableValue}, 289 true, 290 }, 291 292 { 293 "nested list", 294 []interface{}{ 295 "foo", 296 []interface{}{hcl2shim.UnknownVariableValue}, 297 }, 298 true, 299 }, 300 } 301 302 for i, tc := range cases { 303 t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { 304 var w unknownCheckWalker 305 if err := reflectwalk.Walk(tc.Input, &w); err != nil { 306 t.Fatalf("err: %s", err) 307 } 308 309 if w.Unknown != tc.Result { 310 t.Fatalf("bad: %v", w.Unknown) 311 } 312 }) 313 } 314 } 315 316 func TestNewResourceConfigShimmed(t *testing.T) { 317 for _, tc := range []struct { 318 Name string 319 Val cty.Value 320 Schema *configschema.Block 321 Expected *ResourceConfig 322 }{ 323 { 324 Name: "empty object", 325 Val: cty.NullVal(cty.EmptyObject), 326 Schema: &configschema.Block{ 327 Attributes: map[string]*configschema.Attribute{ 328 "foo": { 329 Type: cty.String, 330 Optional: true, 331 }, 332 }, 333 }, 334 Expected: &ResourceConfig{ 335 Raw: map[string]interface{}{}, 336 Config: map[string]interface{}{}, 337 }, 338 }, 339 { 340 Name: "basic", 341 Val: cty.ObjectVal(map[string]cty.Value{ 342 "foo": cty.StringVal("bar"), 343 }), 344 Schema: &configschema.Block{ 345 Attributes: map[string]*configschema.Attribute{ 346 "foo": { 347 Type: cty.String, 348 Optional: true, 349 }, 350 }, 351 }, 352 Expected: &ResourceConfig{ 353 Raw: map[string]interface{}{ 354 "foo": "bar", 355 }, 356 Config: map[string]interface{}{ 357 "foo": "bar", 358 }, 359 }, 360 }, 361 { 362 Name: "null string", 363 Val: cty.ObjectVal(map[string]cty.Value{ 364 "foo": cty.NullVal(cty.String), 365 }), 366 Schema: &configschema.Block{ 367 Attributes: map[string]*configschema.Attribute{ 368 "foo": { 369 Type: cty.String, 370 Optional: true, 371 }, 372 }, 373 }, 374 Expected: &ResourceConfig{ 375 Raw: map[string]interface{}{}, 376 Config: map[string]interface{}{}, 377 }, 378 }, 379 { 380 Name: "unknown string", 381 Val: cty.ObjectVal(map[string]cty.Value{ 382 "foo": cty.UnknownVal(cty.String), 383 }), 384 Schema: &configschema.Block{ 385 Attributes: map[string]*configschema.Attribute{ 386 "foo": { 387 Type: cty.String, 388 Optional: true, 389 }, 390 }, 391 }, 392 Expected: &ResourceConfig{ 393 ComputedKeys: []string{"foo"}, 394 Raw: map[string]interface{}{ 395 "foo": hcl2shim.UnknownVariableValue, 396 }, 397 Config: map[string]interface{}{ 398 "foo": hcl2shim.UnknownVariableValue, 399 }, 400 }, 401 }, 402 { 403 Name: "unknown collections", 404 Val: cty.ObjectVal(map[string]cty.Value{ 405 "bar": cty.UnknownVal(cty.Map(cty.String)), 406 "baz": cty.UnknownVal(cty.List(cty.String)), 407 }), 408 Schema: &configschema.Block{ 409 Attributes: map[string]*configschema.Attribute{ 410 "bar": { 411 Type: cty.Map(cty.String), 412 Required: true, 413 }, 414 "baz": { 415 Type: cty.List(cty.String), 416 Optional: true, 417 }, 418 }, 419 }, 420 Expected: &ResourceConfig{ 421 ComputedKeys: []string{"bar", "baz"}, 422 Raw: map[string]interface{}{ 423 "bar": hcl2shim.UnknownVariableValue, 424 "baz": hcl2shim.UnknownVariableValue, 425 }, 426 Config: map[string]interface{}{ 427 "bar": hcl2shim.UnknownVariableValue, 428 "baz": hcl2shim.UnknownVariableValue, 429 }, 430 }, 431 }, 432 { 433 Name: "null collections", 434 Val: cty.ObjectVal(map[string]cty.Value{ 435 "bar": cty.NullVal(cty.Map(cty.String)), 436 "baz": cty.NullVal(cty.List(cty.String)), 437 }), 438 Schema: &configschema.Block{ 439 Attributes: map[string]*configschema.Attribute{ 440 "bar": { 441 Type: cty.Map(cty.String), 442 Required: true, 443 }, 444 "baz": { 445 Type: cty.List(cty.String), 446 Optional: true, 447 }, 448 }, 449 }, 450 Expected: &ResourceConfig{ 451 Raw: map[string]interface{}{}, 452 Config: map[string]interface{}{}, 453 }, 454 }, 455 { 456 Name: "unknown blocks", 457 Val: cty.ObjectVal(map[string]cty.Value{ 458 "bar": cty.UnknownVal(cty.Map(cty.String)), 459 "baz": cty.UnknownVal(cty.List(cty.String)), 460 }), 461 Schema: &configschema.Block{ 462 BlockTypes: map[string]*configschema.NestedBlock{ 463 "bar": { 464 Block: configschema.Block{}, 465 Nesting: configschema.NestingList, 466 }, 467 "baz": { 468 Block: configschema.Block{}, 469 Nesting: configschema.NestingSet, 470 }, 471 }, 472 }, 473 Expected: &ResourceConfig{ 474 ComputedKeys: []string{"bar", "baz"}, 475 Raw: map[string]interface{}{ 476 "bar": hcl2shim.UnknownVariableValue, 477 "baz": hcl2shim.UnknownVariableValue, 478 }, 479 Config: map[string]interface{}{ 480 "bar": hcl2shim.UnknownVariableValue, 481 "baz": hcl2shim.UnknownVariableValue, 482 }, 483 }, 484 }, 485 { 486 Name: "unknown in nested blocks", 487 Val: cty.ObjectVal(map[string]cty.Value{ 488 "bar": cty.ListVal([]cty.Value{ 489 cty.ObjectVal(map[string]cty.Value{ 490 "baz": cty.ListVal([]cty.Value{ 491 cty.ObjectVal(map[string]cty.Value{ 492 "list": cty.UnknownVal(cty.List(cty.String)), 493 }), 494 }), 495 }), 496 }), 497 }), 498 Schema: &configschema.Block{ 499 BlockTypes: map[string]*configschema.NestedBlock{ 500 "bar": { 501 Block: configschema.Block{ 502 BlockTypes: map[string]*configschema.NestedBlock{ 503 "baz": { 504 Block: configschema.Block{ 505 Attributes: map[string]*configschema.Attribute{ 506 "list": {Type: cty.List(cty.String), 507 Optional: true, 508 }, 509 }, 510 }, 511 Nesting: configschema.NestingList, 512 }, 513 }, 514 }, 515 Nesting: configschema.NestingList, 516 }, 517 }, 518 }, 519 Expected: &ResourceConfig{ 520 ComputedKeys: []string{"bar.0.baz.0.list"}, 521 Raw: map[string]interface{}{ 522 "bar": []interface{}{map[string]interface{}{ 523 "baz": []interface{}{map[string]interface{}{ 524 "list": "74D93920-ED26-11E3-AC10-0800200C9A66", 525 }}, 526 }}, 527 }, 528 Config: map[string]interface{}{ 529 "bar": []interface{}{map[string]interface{}{ 530 "baz": []interface{}{map[string]interface{}{ 531 "list": "74D93920-ED26-11E3-AC10-0800200C9A66", 532 }}, 533 }}, 534 }, 535 }, 536 }, 537 { 538 Name: "unknown in set", 539 Val: cty.ObjectVal(map[string]cty.Value{ 540 "bar": cty.SetVal([]cty.Value{ 541 cty.ObjectVal(map[string]cty.Value{ 542 "val": cty.UnknownVal(cty.String), 543 }), 544 }), 545 }), 546 Schema: &configschema.Block{ 547 BlockTypes: map[string]*configschema.NestedBlock{ 548 "bar": { 549 Block: configschema.Block{ 550 Attributes: map[string]*configschema.Attribute{ 551 "val": { 552 Type: cty.String, 553 Optional: true, 554 }, 555 }, 556 }, 557 Nesting: configschema.NestingSet, 558 }, 559 }, 560 }, 561 Expected: &ResourceConfig{ 562 ComputedKeys: []string{"bar.0.val"}, 563 Raw: map[string]interface{}{ 564 "bar": []interface{}{map[string]interface{}{ 565 "val": "74D93920-ED26-11E3-AC10-0800200C9A66", 566 }}, 567 }, 568 Config: map[string]interface{}{ 569 "bar": []interface{}{map[string]interface{}{ 570 "val": "74D93920-ED26-11E3-AC10-0800200C9A66", 571 }}, 572 }, 573 }, 574 }, 575 { 576 Name: "unknown in attribute sets", 577 Val: cty.ObjectVal(map[string]cty.Value{ 578 "bar": cty.SetVal([]cty.Value{ 579 cty.ObjectVal(map[string]cty.Value{ 580 "val": cty.UnknownVal(cty.String), 581 }), 582 }), 583 "baz": cty.SetVal([]cty.Value{ 584 cty.ObjectVal(map[string]cty.Value{ 585 "obj": cty.UnknownVal(cty.Object(map[string]cty.Type{ 586 "attr": cty.List(cty.String), 587 })), 588 }), 589 cty.ObjectVal(map[string]cty.Value{ 590 "obj": cty.ObjectVal(map[string]cty.Value{ 591 "attr": cty.UnknownVal(cty.List(cty.String)), 592 }), 593 }), 594 }), 595 }), 596 Schema: &configschema.Block{ 597 Attributes: map[string]*configschema.Attribute{ 598 "bar": &configschema.Attribute{ 599 Type: cty.Set(cty.Object(map[string]cty.Type{ 600 "val": cty.String, 601 })), 602 }, 603 "baz": &configschema.Attribute{ 604 Type: cty.Set(cty.Object(map[string]cty.Type{ 605 "obj": cty.Object(map[string]cty.Type{ 606 "attr": cty.List(cty.String), 607 }), 608 })), 609 }, 610 }, 611 }, 612 Expected: &ResourceConfig{ 613 ComputedKeys: []string{"bar.0.val", "baz.0.obj.attr", "baz.1.obj"}, 614 Raw: map[string]interface{}{ 615 "bar": []interface{}{map[string]interface{}{ 616 "val": "74D93920-ED26-11E3-AC10-0800200C9A66", 617 }}, 618 "baz": []interface{}{ 619 map[string]interface{}{ 620 "obj": map[string]interface{}{ 621 "attr": "74D93920-ED26-11E3-AC10-0800200C9A66", 622 }, 623 }, 624 map[string]interface{}{ 625 "obj": "74D93920-ED26-11E3-AC10-0800200C9A66", 626 }, 627 }, 628 }, 629 Config: map[string]interface{}{ 630 "bar": []interface{}{map[string]interface{}{ 631 "val": "74D93920-ED26-11E3-AC10-0800200C9A66", 632 }}, 633 "baz": []interface{}{ 634 map[string]interface{}{ 635 "obj": map[string]interface{}{ 636 "attr": "74D93920-ED26-11E3-AC10-0800200C9A66", 637 }, 638 }, 639 map[string]interface{}{ 640 "obj": "74D93920-ED26-11E3-AC10-0800200C9A66", 641 }, 642 }, 643 }, 644 }, 645 }, 646 { 647 Name: "null blocks", 648 Val: cty.ObjectVal(map[string]cty.Value{ 649 "bar": cty.NullVal(cty.Map(cty.String)), 650 "baz": cty.NullVal(cty.List(cty.String)), 651 }), 652 Schema: &configschema.Block{ 653 BlockTypes: map[string]*configschema.NestedBlock{ 654 "bar": { 655 Block: configschema.Block{}, 656 Nesting: configschema.NestingMap, 657 }, 658 "baz": { 659 Block: configschema.Block{}, 660 Nesting: configschema.NestingSingle, 661 }, 662 }, 663 }, 664 Expected: &ResourceConfig{ 665 Raw: map[string]interface{}{}, 666 Config: map[string]interface{}{}, 667 }, 668 }, 669 } { 670 t.Run(tc.Name, func(*testing.T) { 671 cfg := NewResourceConfigShimmed(tc.Val, tc.Schema) 672 if !tc.Expected.Equal(cfg) { 673 t.Fatalf("expected:\n%#v\ngot:\n%#v", tc.Expected, cfg) 674 } 675 }) 676 } 677 }