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