github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/plans/objchange/compatible_test.go (about) 1 package objchange 2 3 import ( 4 "fmt" 5 "testing" 6 7 "github.com/apparentlymart/go-dump/dump" 8 "github.com/zclconf/go-cty/cty" 9 10 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema" 11 "github.com/hashicorp/terraform-plugin-sdk/internal/tfdiags" 12 ) 13 14 func TestAssertObjectCompatible(t *testing.T) { 15 schemaWithFoo := configschema.Block{ 16 Attributes: map[string]*configschema.Attribute{ 17 "foo": {Type: cty.String, Optional: true}, 18 }, 19 } 20 fooBlockValue := cty.ObjectVal(map[string]cty.Value{ 21 "foo": cty.StringVal("bar"), 22 }) 23 schemaWithFooBar := configschema.Block{ 24 Attributes: map[string]*configschema.Attribute{ 25 "foo": {Type: cty.String, Optional: true}, 26 "bar": {Type: cty.String, Optional: true}, 27 }, 28 } 29 fooBarBlockValue := cty.ObjectVal(map[string]cty.Value{ 30 "foo": cty.StringVal("bar"), 31 "bar": cty.NullVal(cty.String), // simulating the situation where bar isn't set in the config at all 32 }) 33 fooBarBlockDynamicPlaceholder := cty.ObjectVal(map[string]cty.Value{ 34 "foo": cty.UnknownVal(cty.String), 35 "bar": cty.NullVal(cty.String), // simulating the situation where bar isn't set in the config at all 36 }) 37 38 tests := []struct { 39 Schema *configschema.Block 40 Planned cty.Value 41 Actual cty.Value 42 WantErrs []string 43 }{ 44 { 45 &configschema.Block{}, 46 cty.EmptyObjectVal, 47 cty.EmptyObjectVal, 48 nil, 49 }, 50 { 51 &configschema.Block{ 52 Attributes: map[string]*configschema.Attribute{ 53 "id": { 54 Type: cty.String, 55 Computed: true, 56 }, 57 "name": { 58 Type: cty.String, 59 Required: true, 60 }, 61 }, 62 }, 63 cty.ObjectVal(map[string]cty.Value{ 64 "id": cty.UnknownVal(cty.String), 65 "name": cty.StringVal("thingy"), 66 }), 67 cty.ObjectVal(map[string]cty.Value{ 68 "id": cty.UnknownVal(cty.String), 69 "name": cty.StringVal("thingy"), 70 }), 71 nil, 72 }, 73 { 74 &configschema.Block{ 75 Attributes: map[string]*configschema.Attribute{ 76 "id": { 77 Type: cty.String, 78 Computed: true, 79 }, 80 "name": { 81 Type: cty.String, 82 Required: true, 83 }, 84 }, 85 }, 86 cty.ObjectVal(map[string]cty.Value{ 87 "id": cty.UnknownVal(cty.String), 88 "name": cty.UnknownVal(cty.String), 89 }), 90 cty.ObjectVal(map[string]cty.Value{ 91 "id": cty.UnknownVal(cty.String), 92 "name": cty.StringVal("thingy"), 93 }), 94 nil, 95 }, 96 { 97 &configschema.Block{ 98 Attributes: map[string]*configschema.Attribute{ 99 "id": { 100 Type: cty.String, 101 Computed: true, 102 }, 103 "name": { 104 Type: cty.String, 105 Required: true, 106 }, 107 }, 108 }, 109 cty.ObjectVal(map[string]cty.Value{ 110 "id": cty.UnknownVal(cty.String), 111 "name": cty.StringVal("wotsit"), 112 }), 113 cty.ObjectVal(map[string]cty.Value{ 114 "id": cty.UnknownVal(cty.String), 115 "name": cty.StringVal("thingy"), 116 }), 117 []string{ 118 `.name: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`, 119 }, 120 }, 121 { 122 &configschema.Block{ 123 Attributes: map[string]*configschema.Attribute{ 124 "id": { 125 Type: cty.String, 126 Computed: true, 127 }, 128 "name": { 129 Type: cty.String, 130 Required: true, 131 Sensitive: true, 132 }, 133 }, 134 }, 135 cty.ObjectVal(map[string]cty.Value{ 136 "id": cty.UnknownVal(cty.String), 137 "name": cty.StringVal("wotsit"), 138 }), 139 cty.ObjectVal(map[string]cty.Value{ 140 "id": cty.UnknownVal(cty.String), 141 "name": cty.StringVal("thingy"), 142 }), 143 []string{ 144 `.name: inconsistent values for sensitive attribute`, 145 }, 146 }, 147 { 148 &configschema.Block{ 149 Attributes: map[string]*configschema.Attribute{ 150 "id": { 151 Type: cty.String, 152 Computed: true, 153 }, 154 "stuff": { 155 Type: cty.DynamicPseudoType, 156 Required: true, 157 }, 158 }, 159 }, 160 cty.ObjectVal(map[string]cty.Value{ 161 "id": cty.UnknownVal(cty.String), 162 "stuff": cty.DynamicVal, 163 }), 164 cty.ObjectVal(map[string]cty.Value{ 165 "id": cty.UnknownVal(cty.String), 166 "stuff": cty.StringVal("thingy"), 167 }), 168 []string{}, 169 }, 170 { 171 &configschema.Block{ 172 Attributes: map[string]*configschema.Attribute{ 173 "id": { 174 Type: cty.String, 175 Computed: true, 176 }, 177 "stuff": { 178 Type: cty.DynamicPseudoType, 179 Required: true, 180 }, 181 }, 182 }, 183 cty.ObjectVal(map[string]cty.Value{ 184 "id": cty.UnknownVal(cty.String), 185 "stuff": cty.StringVal("wotsit"), 186 }), 187 cty.ObjectVal(map[string]cty.Value{ 188 "id": cty.UnknownVal(cty.String), 189 "stuff": cty.StringVal("thingy"), 190 }), 191 []string{ 192 `.stuff: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`, 193 }, 194 }, 195 { 196 &configschema.Block{ 197 Attributes: map[string]*configschema.Attribute{ 198 "id": { 199 Type: cty.String, 200 Computed: true, 201 }, 202 "stuff": { 203 Type: cty.DynamicPseudoType, 204 Required: true, 205 }, 206 }, 207 }, 208 cty.ObjectVal(map[string]cty.Value{ 209 "id": cty.UnknownVal(cty.String), 210 "stuff": cty.StringVal("true"), 211 }), 212 cty.ObjectVal(map[string]cty.Value{ 213 "id": cty.UnknownVal(cty.String), 214 "stuff": cty.True, 215 }), 216 []string{ 217 `.stuff: wrong final value type: string required`, 218 }, 219 }, 220 { 221 &configschema.Block{ 222 Attributes: map[string]*configschema.Attribute{ 223 "id": { 224 Type: cty.String, 225 Computed: true, 226 }, 227 "stuff": { 228 Type: cty.DynamicPseudoType, 229 Required: true, 230 }, 231 }, 232 }, 233 cty.ObjectVal(map[string]cty.Value{ 234 "id": cty.UnknownVal(cty.String), 235 "stuff": cty.DynamicVal, 236 }), 237 cty.ObjectVal(map[string]cty.Value{ 238 "id": cty.UnknownVal(cty.String), 239 "stuff": cty.EmptyObjectVal, 240 }), 241 nil, 242 }, 243 { 244 &configschema.Block{ 245 Attributes: map[string]*configschema.Attribute{ 246 "id": { 247 Type: cty.String, 248 Computed: true, 249 }, 250 "stuff": { 251 Type: cty.DynamicPseudoType, 252 Required: true, 253 }, 254 }, 255 }, 256 cty.ObjectVal(map[string]cty.Value{ 257 "id": cty.UnknownVal(cty.String), 258 "stuff": cty.ObjectVal(map[string]cty.Value{ 259 "nonsense": cty.StringVal("yup"), 260 }), 261 }), 262 cty.ObjectVal(map[string]cty.Value{ 263 "id": cty.UnknownVal(cty.String), 264 "stuff": cty.EmptyObjectVal, 265 }), 266 []string{ 267 `.stuff: wrong final value type: attribute "nonsense" is required`, 268 }, 269 }, 270 { 271 &configschema.Block{ 272 Attributes: map[string]*configschema.Attribute{ 273 "id": { 274 Type: cty.String, 275 Computed: true, 276 }, 277 "tags": { 278 Type: cty.Map(cty.String), 279 Optional: true, 280 }, 281 }, 282 }, 283 cty.ObjectVal(map[string]cty.Value{ 284 "id": cty.UnknownVal(cty.String), 285 "tags": cty.MapVal(map[string]cty.Value{ 286 "Name": cty.StringVal("thingy"), 287 }), 288 }), 289 cty.ObjectVal(map[string]cty.Value{ 290 "id": cty.UnknownVal(cty.String), 291 "tags": cty.MapVal(map[string]cty.Value{ 292 "Name": cty.StringVal("thingy"), 293 }), 294 }), 295 nil, 296 }, 297 { 298 &configschema.Block{ 299 Attributes: map[string]*configschema.Attribute{ 300 "id": { 301 Type: cty.String, 302 Computed: true, 303 }, 304 "tags": { 305 Type: cty.Map(cty.String), 306 Optional: true, 307 }, 308 }, 309 }, 310 cty.ObjectVal(map[string]cty.Value{ 311 "id": cty.UnknownVal(cty.String), 312 "tags": cty.MapVal(map[string]cty.Value{ 313 "Name": cty.UnknownVal(cty.String), 314 }), 315 }), 316 cty.ObjectVal(map[string]cty.Value{ 317 "id": cty.UnknownVal(cty.String), 318 "tags": cty.MapVal(map[string]cty.Value{ 319 "Name": cty.StringVal("thingy"), 320 }), 321 }), 322 nil, 323 }, 324 { 325 &configschema.Block{ 326 Attributes: map[string]*configschema.Attribute{ 327 "id": { 328 Type: cty.String, 329 Computed: true, 330 }, 331 "tags": { 332 Type: cty.Map(cty.String), 333 Optional: true, 334 }, 335 }, 336 }, 337 cty.ObjectVal(map[string]cty.Value{ 338 "id": cty.UnknownVal(cty.String), 339 "tags": cty.MapVal(map[string]cty.Value{ 340 "Name": cty.StringVal("wotsit"), 341 }), 342 }), 343 cty.ObjectVal(map[string]cty.Value{ 344 "id": cty.UnknownVal(cty.String), 345 "tags": cty.MapVal(map[string]cty.Value{ 346 "Name": cty.StringVal("thingy"), 347 }), 348 }), 349 []string{ 350 `.tags["Name"]: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`, 351 }, 352 }, 353 { 354 &configschema.Block{ 355 Attributes: map[string]*configschema.Attribute{ 356 "id": { 357 Type: cty.String, 358 Computed: true, 359 }, 360 "tags": { 361 Type: cty.Map(cty.String), 362 Optional: true, 363 }, 364 }, 365 }, 366 cty.ObjectVal(map[string]cty.Value{ 367 "id": cty.UnknownVal(cty.String), 368 "tags": cty.MapVal(map[string]cty.Value{ 369 "Name": cty.StringVal("thingy"), 370 }), 371 }), 372 cty.ObjectVal(map[string]cty.Value{ 373 "id": cty.UnknownVal(cty.String), 374 "tags": cty.MapVal(map[string]cty.Value{ 375 "Name": cty.StringVal("thingy"), 376 "Env": cty.StringVal("production"), 377 }), 378 }), 379 []string{ 380 `.tags: new element "Env" has appeared`, 381 }, 382 }, 383 { 384 &configschema.Block{ 385 Attributes: map[string]*configschema.Attribute{ 386 "id": { 387 Type: cty.String, 388 Computed: true, 389 }, 390 "tags": { 391 Type: cty.Map(cty.String), 392 Optional: true, 393 }, 394 }, 395 }, 396 cty.ObjectVal(map[string]cty.Value{ 397 "id": cty.UnknownVal(cty.String), 398 "tags": cty.MapVal(map[string]cty.Value{ 399 "Name": cty.StringVal("thingy"), 400 }), 401 }), 402 cty.ObjectVal(map[string]cty.Value{ 403 "id": cty.UnknownVal(cty.String), 404 "tags": cty.MapValEmpty(cty.String), 405 }), 406 []string{ 407 `.tags: element "Name" has vanished`, 408 }, 409 }, 410 { 411 &configschema.Block{ 412 Attributes: map[string]*configschema.Attribute{ 413 "id": { 414 Type: cty.String, 415 Computed: true, 416 }, 417 "tags": { 418 Type: cty.Map(cty.String), 419 Optional: true, 420 }, 421 }, 422 }, 423 cty.ObjectVal(map[string]cty.Value{ 424 "id": cty.UnknownVal(cty.String), 425 "tags": cty.MapVal(map[string]cty.Value{ 426 "Name": cty.UnknownVal(cty.String), 427 }), 428 }), 429 cty.ObjectVal(map[string]cty.Value{ 430 "id": cty.UnknownVal(cty.String), 431 "tags": cty.MapVal(map[string]cty.Value{ 432 "Name": cty.NullVal(cty.String), 433 }), 434 }), 435 nil, 436 }, 437 { 438 &configschema.Block{ 439 Attributes: map[string]*configschema.Attribute{ 440 "id": { 441 Type: cty.String, 442 Computed: true, 443 }, 444 "zones": { 445 Type: cty.Set(cty.String), 446 Optional: true, 447 }, 448 }, 449 }, 450 cty.ObjectVal(map[string]cty.Value{ 451 "id": cty.UnknownVal(cty.String), 452 "zones": cty.SetVal([]cty.Value{ 453 cty.StringVal("thingy"), 454 }), 455 }), 456 cty.ObjectVal(map[string]cty.Value{ 457 "id": cty.UnknownVal(cty.String), 458 "zones": cty.SetVal([]cty.Value{ 459 cty.StringVal("thingy"), 460 }), 461 }), 462 nil, 463 }, 464 { 465 &configschema.Block{ 466 Attributes: map[string]*configschema.Attribute{ 467 "id": { 468 Type: cty.String, 469 Computed: true, 470 }, 471 "zones": { 472 Type: cty.Set(cty.String), 473 Optional: true, 474 }, 475 }, 476 }, 477 cty.ObjectVal(map[string]cty.Value{ 478 "id": cty.UnknownVal(cty.String), 479 "zones": cty.SetVal([]cty.Value{ 480 cty.StringVal("thingy"), 481 }), 482 }), 483 cty.ObjectVal(map[string]cty.Value{ 484 "id": cty.UnknownVal(cty.String), 485 "zones": cty.SetVal([]cty.Value{ 486 cty.StringVal("thingy"), 487 cty.StringVal("wotsit"), 488 }), 489 }), 490 []string{ 491 `.zones: actual set element cty.StringVal("wotsit") does not correlate with any element in plan`, 492 `.zones: length changed from 1 to 2`, 493 }, 494 }, 495 { 496 &configschema.Block{ 497 Attributes: map[string]*configschema.Attribute{ 498 "id": { 499 Type: cty.String, 500 Computed: true, 501 }, 502 "zones": { 503 Type: cty.Set(cty.String), 504 Optional: true, 505 }, 506 }, 507 }, 508 cty.ObjectVal(map[string]cty.Value{ 509 "id": cty.UnknownVal(cty.String), 510 "zones": cty.SetVal([]cty.Value{ 511 cty.UnknownVal(cty.String), 512 cty.UnknownVal(cty.String), 513 }), 514 }), 515 cty.ObjectVal(map[string]cty.Value{ 516 "id": cty.UnknownVal(cty.String), 517 "zones": cty.SetVal([]cty.Value{ 518 // Imagine that both of our unknown values ultimately resolved to "thingy", 519 // causing them to collapse into a single element. That's valid, 520 // even though it's also a little confusing and counter-intuitive. 521 cty.StringVal("thingy"), 522 }), 523 }), 524 nil, 525 }, 526 { 527 &configschema.Block{ 528 Attributes: map[string]*configschema.Attribute{ 529 "id": { 530 Type: cty.String, 531 Computed: true, 532 }, 533 "names": { 534 Type: cty.List(cty.String), 535 Optional: true, 536 }, 537 }, 538 }, 539 cty.ObjectVal(map[string]cty.Value{ 540 "id": cty.UnknownVal(cty.String), 541 "names": cty.ListVal([]cty.Value{ 542 cty.StringVal("thingy"), 543 }), 544 }), 545 cty.ObjectVal(map[string]cty.Value{ 546 "id": cty.UnknownVal(cty.String), 547 "names": cty.ListVal([]cty.Value{ 548 cty.StringVal("thingy"), 549 }), 550 }), 551 nil, 552 }, 553 { 554 &configschema.Block{ 555 Attributes: map[string]*configschema.Attribute{ 556 "id": { 557 Type: cty.String, 558 Computed: true, 559 }, 560 "names": { 561 Type: cty.List(cty.String), 562 Optional: true, 563 }, 564 }, 565 }, 566 cty.ObjectVal(map[string]cty.Value{ 567 "id": cty.UnknownVal(cty.String), 568 "names": cty.UnknownVal(cty.List(cty.String)), 569 }), 570 cty.ObjectVal(map[string]cty.Value{ 571 "id": cty.UnknownVal(cty.String), 572 "names": cty.ListVal([]cty.Value{ 573 cty.StringVal("thingy"), 574 }), 575 }), 576 nil, 577 }, 578 { 579 &configschema.Block{ 580 Attributes: map[string]*configschema.Attribute{ 581 "id": { 582 Type: cty.String, 583 Computed: true, 584 }, 585 "names": { 586 Type: cty.List(cty.String), 587 Optional: true, 588 }, 589 }, 590 }, 591 cty.ObjectVal(map[string]cty.Value{ 592 "id": cty.UnknownVal(cty.String), 593 "names": cty.ListVal([]cty.Value{ 594 cty.UnknownVal(cty.String), 595 }), 596 }), 597 cty.ObjectVal(map[string]cty.Value{ 598 "id": cty.UnknownVal(cty.String), 599 "names": cty.ListVal([]cty.Value{ 600 cty.StringVal("thingy"), 601 }), 602 }), 603 nil, 604 }, 605 { 606 &configschema.Block{ 607 Attributes: map[string]*configschema.Attribute{ 608 "id": { 609 Type: cty.String, 610 Computed: true, 611 }, 612 "names": { 613 Type: cty.List(cty.String), 614 Optional: true, 615 }, 616 }, 617 }, 618 cty.ObjectVal(map[string]cty.Value{ 619 "id": cty.UnknownVal(cty.String), 620 "names": cty.ListVal([]cty.Value{ 621 cty.StringVal("thingy"), 622 cty.UnknownVal(cty.String), 623 }), 624 }), 625 cty.ObjectVal(map[string]cty.Value{ 626 "id": cty.UnknownVal(cty.String), 627 "names": cty.ListVal([]cty.Value{ 628 cty.StringVal("thingy"), 629 cty.StringVal("wotsit"), 630 }), 631 }), 632 nil, 633 }, 634 { 635 &configschema.Block{ 636 Attributes: map[string]*configschema.Attribute{ 637 "id": { 638 Type: cty.String, 639 Computed: true, 640 }, 641 "names": { 642 Type: cty.List(cty.String), 643 Optional: true, 644 }, 645 }, 646 }, 647 cty.ObjectVal(map[string]cty.Value{ 648 "id": cty.UnknownVal(cty.String), 649 "names": cty.ListVal([]cty.Value{ 650 cty.UnknownVal(cty.String), 651 cty.StringVal("thingy"), 652 }), 653 }), 654 cty.ObjectVal(map[string]cty.Value{ 655 "id": cty.UnknownVal(cty.String), 656 "names": cty.ListVal([]cty.Value{ 657 cty.StringVal("thingy"), 658 cty.StringVal("wotsit"), 659 }), 660 }), 661 []string{ 662 `.names[1]: was cty.StringVal("thingy"), but now cty.StringVal("wotsit")`, 663 }, 664 }, 665 { 666 &configschema.Block{ 667 Attributes: map[string]*configschema.Attribute{ 668 "id": { 669 Type: cty.String, 670 Computed: true, 671 }, 672 "names": { 673 Type: cty.List(cty.String), 674 Optional: true, 675 }, 676 }, 677 }, 678 cty.ObjectVal(map[string]cty.Value{ 679 "id": cty.UnknownVal(cty.String), 680 "names": cty.ListVal([]cty.Value{ 681 cty.UnknownVal(cty.String), 682 }), 683 }), 684 cty.ObjectVal(map[string]cty.Value{ 685 "id": cty.UnknownVal(cty.String), 686 "names": cty.ListVal([]cty.Value{ 687 cty.StringVal("thingy"), 688 cty.StringVal("wotsit"), 689 }), 690 }), 691 []string{ 692 `.names: new element 1 has appeared`, 693 }, 694 }, 695 696 // NestingSingle blocks 697 { 698 &configschema.Block{ 699 BlockTypes: map[string]*configschema.NestedBlock{ 700 "key": { 701 Nesting: configschema.NestingSingle, 702 Block: configschema.Block{}, 703 }, 704 }, 705 }, 706 cty.ObjectVal(map[string]cty.Value{ 707 "key": cty.EmptyObjectVal, 708 }), 709 cty.ObjectVal(map[string]cty.Value{ 710 "key": cty.EmptyObjectVal, 711 }), 712 nil, 713 }, 714 { 715 &configschema.Block{ 716 BlockTypes: map[string]*configschema.NestedBlock{ 717 "key": { 718 Nesting: configschema.NestingSingle, 719 Block: configschema.Block{}, 720 }, 721 }, 722 }, 723 cty.ObjectVal(map[string]cty.Value{ 724 "key": cty.UnknownVal(cty.EmptyObject), 725 }), 726 cty.ObjectVal(map[string]cty.Value{ 727 "key": cty.EmptyObjectVal, 728 }), 729 nil, 730 }, 731 { 732 &configschema.Block{ 733 BlockTypes: map[string]*configschema.NestedBlock{ 734 "key": { 735 Nesting: configschema.NestingSingle, 736 Block: configschema.Block{ 737 Attributes: map[string]*configschema.Attribute{ 738 "foo": { 739 Type: cty.String, 740 Optional: true, 741 }, 742 }, 743 }, 744 }, 745 }, 746 }, 747 cty.ObjectVal(map[string]cty.Value{ 748 "key": cty.NullVal(cty.Object(map[string]cty.Type{ 749 "foo": cty.String, 750 })), 751 }), 752 cty.ObjectVal(map[string]cty.Value{ 753 "key": cty.ObjectVal(map[string]cty.Value{ 754 "foo": cty.StringVal("hello"), 755 }), 756 }), 757 []string{ 758 `.key: was absent, but now present`, 759 }, 760 }, 761 { 762 &configschema.Block{ 763 BlockTypes: map[string]*configschema.NestedBlock{ 764 "key": { 765 Nesting: configschema.NestingSingle, 766 Block: configschema.Block{ 767 Attributes: map[string]*configschema.Attribute{ 768 "foo": { 769 Type: cty.String, 770 Optional: true, 771 }, 772 }, 773 }, 774 }, 775 }, 776 }, 777 cty.ObjectVal(map[string]cty.Value{ 778 "key": cty.ObjectVal(map[string]cty.Value{ 779 "foo": cty.StringVal("hello"), 780 }), 781 }), 782 cty.ObjectVal(map[string]cty.Value{ 783 "key": cty.NullVal(cty.Object(map[string]cty.Type{ 784 "foo": cty.String, 785 })), 786 }), 787 []string{ 788 `.key: was present, but now absent`, 789 }, 790 }, 791 { 792 &configschema.Block{ 793 BlockTypes: map[string]*configschema.NestedBlock{ 794 "key": { 795 Nesting: configschema.NestingSingle, 796 Block: configschema.Block{ 797 Attributes: map[string]*configschema.Attribute{ 798 "foo": { 799 Type: cty.String, 800 Optional: true, 801 }, 802 }, 803 }, 804 }, 805 }, 806 }, 807 cty.ObjectVal(map[string]cty.Value{ 808 "key": cty.ObjectVal(map[string]cty.Value{ 809 // One wholly unknown block is what "dynamic" blocks 810 // generate when the for_each expression is unknown. 811 "foo": cty.UnknownVal(cty.String), 812 }), 813 }), 814 cty.ObjectVal(map[string]cty.Value{ 815 "key": cty.NullVal(cty.Object(map[string]cty.Type{ 816 "foo": cty.String, 817 })), 818 }), 819 nil, 820 }, 821 822 // NestingList blocks 823 { 824 &configschema.Block{ 825 BlockTypes: map[string]*configschema.NestedBlock{ 826 "key": { 827 Nesting: configschema.NestingList, 828 Block: schemaWithFoo, 829 }, 830 }, 831 }, 832 cty.ObjectVal(map[string]cty.Value{ 833 "key": cty.ListVal([]cty.Value{ 834 fooBlockValue, 835 }), 836 }), 837 cty.ObjectVal(map[string]cty.Value{ 838 "key": cty.ListVal([]cty.Value{ 839 fooBlockValue, 840 }), 841 }), 842 nil, 843 }, 844 { 845 &configschema.Block{ 846 BlockTypes: map[string]*configschema.NestedBlock{ 847 "key": { 848 Nesting: configschema.NestingList, 849 Block: schemaWithFoo, 850 }, 851 }, 852 }, 853 cty.ObjectVal(map[string]cty.Value{ 854 "key": cty.TupleVal([]cty.Value{ 855 fooBlockValue, 856 fooBlockValue, 857 }), 858 }), 859 cty.ObjectVal(map[string]cty.Value{ 860 "key": cty.TupleVal([]cty.Value{ 861 fooBlockValue, 862 }), 863 }), 864 []string{ 865 `.key: block count changed from 2 to 1`, 866 }, 867 }, 868 { 869 &configschema.Block{ 870 BlockTypes: map[string]*configschema.NestedBlock{ 871 "key": { 872 Nesting: configschema.NestingList, 873 Block: schemaWithFoo, 874 }, 875 }, 876 }, 877 cty.ObjectVal(map[string]cty.Value{ 878 "key": cty.TupleVal([]cty.Value{}), 879 }), 880 cty.ObjectVal(map[string]cty.Value{ 881 "key": cty.TupleVal([]cty.Value{ 882 fooBlockValue, 883 fooBlockValue, 884 }), 885 }), 886 []string{ 887 `.key: block count changed from 0 to 2`, 888 }, 889 }, 890 { 891 &configschema.Block{ 892 BlockTypes: map[string]*configschema.NestedBlock{ 893 "key": { 894 Nesting: configschema.NestingList, 895 Block: schemaWithFooBar, 896 }, 897 }, 898 }, 899 cty.ObjectVal(map[string]cty.Value{ 900 "key": cty.ListVal([]cty.Value{ 901 fooBarBlockDynamicPlaceholder, // the presence of this disables some of our checks 902 }), 903 }), 904 cty.ObjectVal(map[string]cty.Value{ 905 "key": cty.ListVal([]cty.Value{ 906 cty.ObjectVal(map[string]cty.Value{ 907 "foo": cty.StringVal("hello"), 908 }), 909 cty.ObjectVal(map[string]cty.Value{ 910 "foo": cty.StringVal("world"), 911 }), 912 }), 913 }), 914 nil, // a single block whose attrs are all unknown is allowed to expand into multiple, because that's how dynamic blocks behave when for_each is unknown 915 }, 916 { 917 &configschema.Block{ 918 BlockTypes: map[string]*configschema.NestedBlock{ 919 "key": { 920 Nesting: configschema.NestingList, 921 Block: schemaWithFooBar, 922 }, 923 }, 924 }, 925 cty.ObjectVal(map[string]cty.Value{ 926 "key": cty.ListVal([]cty.Value{ 927 fooBarBlockValue, // the presence of one static block does not negate that the following element looks like a dynamic placeholder 928 fooBarBlockDynamicPlaceholder, // the presence of this disables some of our checks 929 }), 930 }), 931 cty.ObjectVal(map[string]cty.Value{ 932 "key": cty.ListVal([]cty.Value{ 933 fooBlockValue, 934 cty.ObjectVal(map[string]cty.Value{ 935 "foo": cty.StringVal("hello"), 936 }), 937 cty.ObjectVal(map[string]cty.Value{ 938 "foo": cty.StringVal("world"), 939 }), 940 }), 941 }), 942 nil, // as above, the presence of a block whose attrs are all unknown indicates dynamic block expansion, so our usual count checks don't apply 943 }, 944 945 // NestingSet blocks 946 { 947 &configschema.Block{ 948 BlockTypes: map[string]*configschema.NestedBlock{ 949 "block": { 950 Nesting: configschema.NestingSet, 951 Block: schemaWithFoo, 952 }, 953 }, 954 }, 955 cty.ObjectVal(map[string]cty.Value{ 956 "block": cty.SetVal([]cty.Value{ 957 cty.ObjectVal(map[string]cty.Value{ 958 "foo": cty.StringVal("hello"), 959 }), 960 cty.ObjectVal(map[string]cty.Value{ 961 "foo": cty.StringVal("world"), 962 }), 963 }), 964 }), 965 cty.ObjectVal(map[string]cty.Value{ 966 "block": cty.SetVal([]cty.Value{ 967 cty.ObjectVal(map[string]cty.Value{ 968 "foo": cty.StringVal("hello"), 969 }), 970 cty.ObjectVal(map[string]cty.Value{ 971 "foo": cty.StringVal("world"), 972 }), 973 }), 974 }), 975 nil, 976 }, 977 { 978 &configschema.Block{ 979 BlockTypes: map[string]*configschema.NestedBlock{ 980 "block": { 981 Nesting: configschema.NestingSet, 982 Block: schemaWithFoo, 983 }, 984 }, 985 }, 986 cty.ObjectVal(map[string]cty.Value{ 987 "block": cty.SetVal([]cty.Value{ 988 cty.ObjectVal(map[string]cty.Value{ 989 "foo": cty.UnknownVal(cty.String), 990 }), 991 cty.ObjectVal(map[string]cty.Value{ 992 "foo": cty.UnknownVal(cty.String), 993 }), 994 }), 995 }), 996 cty.ObjectVal(map[string]cty.Value{ 997 "block": cty.SetVal([]cty.Value{ 998 // This is testing the scenario where the two unknown values 999 // turned out to be equal after we learned their values, 1000 // and so they coalesced together into a single element. 1001 cty.ObjectVal(map[string]cty.Value{ 1002 "foo": cty.StringVal("hello"), 1003 }), 1004 }), 1005 }), 1006 nil, 1007 }, 1008 { 1009 &configschema.Block{ 1010 BlockTypes: map[string]*configschema.NestedBlock{ 1011 "block": { 1012 Nesting: configschema.NestingSet, 1013 Block: schemaWithFoo, 1014 }, 1015 }, 1016 }, 1017 cty.ObjectVal(map[string]cty.Value{ 1018 "block": cty.SetVal([]cty.Value{ 1019 cty.ObjectVal(map[string]cty.Value{ 1020 "foo": cty.UnknownVal(cty.String), 1021 }), 1022 cty.ObjectVal(map[string]cty.Value{ 1023 "foo": cty.UnknownVal(cty.String), 1024 }), 1025 }), 1026 }), 1027 cty.ObjectVal(map[string]cty.Value{ 1028 "block": cty.SetVal([]cty.Value{ 1029 cty.ObjectVal(map[string]cty.Value{ 1030 "foo": cty.StringVal("hello"), 1031 }), 1032 cty.ObjectVal(map[string]cty.Value{ 1033 "foo": cty.StringVal("world"), 1034 }), 1035 }), 1036 }), 1037 nil, 1038 }, 1039 { 1040 &configschema.Block{ 1041 BlockTypes: map[string]*configschema.NestedBlock{ 1042 "block": { 1043 Nesting: configschema.NestingSet, 1044 Block: schemaWithFoo, 1045 }, 1046 }, 1047 }, 1048 cty.ObjectVal(map[string]cty.Value{ 1049 "block": cty.SetVal([]cty.Value{ 1050 cty.ObjectVal(map[string]cty.Value{ 1051 "foo": cty.UnknownVal(cty.String), 1052 }), 1053 cty.ObjectVal(map[string]cty.Value{ 1054 "foo": cty.UnknownVal(cty.String), 1055 }), 1056 }), 1057 }), 1058 cty.ObjectVal(map[string]cty.Value{ 1059 "block": cty.SetVal([]cty.Value{ 1060 cty.ObjectVal(map[string]cty.Value{ 1061 "foo": cty.StringVal("hello"), 1062 }), 1063 cty.ObjectVal(map[string]cty.Value{ 1064 "foo": cty.StringVal("world"), 1065 }), 1066 cty.ObjectVal(map[string]cty.Value{ 1067 "foo": cty.StringVal("nope"), 1068 }), 1069 }), 1070 }), 1071 // there is no error here, because the presence of unknowns 1072 // indicates this may be a dynamic block, and the length is unknown 1073 nil, 1074 }, 1075 { 1076 &configschema.Block{ 1077 BlockTypes: map[string]*configschema.NestedBlock{ 1078 "block": { 1079 Nesting: configschema.NestingSet, 1080 Block: schemaWithFoo, 1081 }, 1082 }, 1083 }, 1084 cty.ObjectVal(map[string]cty.Value{ 1085 "block": cty.SetVal([]cty.Value{ 1086 cty.ObjectVal(map[string]cty.Value{ 1087 "foo": cty.StringVal("hello"), 1088 }), 1089 cty.ObjectVal(map[string]cty.Value{ 1090 "foo": cty.StringVal("world"), 1091 }), 1092 }), 1093 }), 1094 cty.ObjectVal(map[string]cty.Value{ 1095 "block": cty.SetVal([]cty.Value{ 1096 cty.ObjectVal(map[string]cty.Value{ 1097 "foo": cty.StringVal("howdy"), 1098 }), 1099 cty.ObjectVal(map[string]cty.Value{ 1100 "foo": cty.StringVal("world"), 1101 }), 1102 }), 1103 }), 1104 []string{ 1105 `.block: planned set element cty.ObjectVal(map[string]cty.Value{"foo":cty.StringVal("hello")}) does not correlate with any element in actual`, 1106 }, 1107 }, 1108 { 1109 // This one is an odd situation where the value representing the 1110 // block itself is unknown. This is never supposed to be true, 1111 // but in legacy SDK mode we allow such things to pass through as 1112 // a warning, and so we must tolerate them for matching purposes. 1113 &configschema.Block{ 1114 BlockTypes: map[string]*configschema.NestedBlock{ 1115 "block": { 1116 Nesting: configschema.NestingSet, 1117 Block: schemaWithFoo, 1118 }, 1119 }, 1120 }, 1121 cty.ObjectVal(map[string]cty.Value{ 1122 "block": cty.SetVal([]cty.Value{ 1123 cty.ObjectVal(map[string]cty.Value{ 1124 "foo": cty.UnknownVal(cty.String), 1125 }), 1126 cty.ObjectVal(map[string]cty.Value{ 1127 "foo": cty.UnknownVal(cty.String), 1128 }), 1129 }), 1130 }), 1131 cty.ObjectVal(map[string]cty.Value{ 1132 "block": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ 1133 "foo": cty.String, 1134 }))), 1135 }), 1136 nil, 1137 }, 1138 { 1139 &configschema.Block{ 1140 BlockTypes: map[string]*configschema.NestedBlock{ 1141 "block": { 1142 Nesting: configschema.NestingSet, 1143 Block: schemaWithFoo, 1144 }, 1145 }, 1146 }, 1147 cty.ObjectVal(map[string]cty.Value{ 1148 "block": cty.SetVal([]cty.Value{ 1149 cty.ObjectVal(map[string]cty.Value{ 1150 "foo": cty.UnknownVal(cty.String), 1151 }), 1152 }), 1153 }), 1154 cty.ObjectVal(map[string]cty.Value{ 1155 "block": cty.SetVal([]cty.Value{ 1156 cty.ObjectVal(map[string]cty.Value{ 1157 "foo": cty.StringVal("a"), 1158 }), 1159 cty.ObjectVal(map[string]cty.Value{ 1160 "foo": cty.StringVal("b"), 1161 }), 1162 }), 1163 }), 1164 nil, 1165 }, 1166 { 1167 &configschema.Block{ 1168 BlockTypes: map[string]*configschema.NestedBlock{ 1169 "block": { 1170 Nesting: configschema.NestingList, 1171 Block: configschema.Block{ 1172 Attributes: map[string]*configschema.Attribute{ 1173 "foo": { 1174 Type: cty.String, 1175 Required: true, 1176 }, 1177 }, 1178 }, 1179 }, 1180 }, 1181 }, 1182 cty.ObjectVal(map[string]cty.Value{ 1183 "block": cty.EmptyObjectVal, 1184 }), 1185 cty.ObjectVal(map[string]cty.Value{ 1186 "block": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ 1187 "foo": cty.String, 1188 }))), 1189 }), 1190 nil, 1191 }, 1192 } 1193 1194 for i, test := range tests { 1195 t.Run(fmt.Sprintf("%02d: %#v and %#v", i, test.Planned, test.Actual), func(t *testing.T) { 1196 errs := AssertObjectCompatible(test.Schema, test.Planned, test.Actual) 1197 1198 wantErrs := make(map[string]struct{}) 1199 gotErrs := make(map[string]struct{}) 1200 for _, err := range errs { 1201 gotErrs[tfdiags.FormatError(err)] = struct{}{} 1202 } 1203 for _, msg := range test.WantErrs { 1204 wantErrs[msg] = struct{}{} 1205 } 1206 1207 t.Logf("\nplanned: %sactual: %s", dump.Value(test.Planned), dump.Value(test.Actual)) 1208 for msg := range wantErrs { 1209 if _, ok := gotErrs[msg]; !ok { 1210 t.Errorf("missing expected error: %s", msg) 1211 } 1212 } 1213 for msg := range gotErrs { 1214 if _, ok := wantErrs[msg]; !ok { 1215 t.Errorf("unexpected extra error: %s", msg) 1216 } 1217 } 1218 }) 1219 } 1220 }