github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/plans/objchange/compatible_test.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package objchange 5 6 import ( 7 "fmt" 8 "testing" 9 10 "github.com/apparentlymart/go-dump/dump" 11 "github.com/zclconf/go-cty/cty" 12 13 "github.com/terramate-io/tf/configs/configschema" 14 "github.com/terramate-io/tf/lang/marks" 15 "github.com/terramate-io/tf/tfdiags" 16 ) 17 18 func TestAssertObjectCompatible(t *testing.T) { 19 schemaWithFoo := configschema.Block{ 20 Attributes: map[string]*configschema.Attribute{ 21 "foo": {Type: cty.String, Optional: true}, 22 }, 23 } 24 fooBlockValue := cty.ObjectVal(map[string]cty.Value{ 25 "foo": cty.StringVal("bar"), 26 }) 27 schemaWithFooBar := configschema.Block{ 28 Attributes: map[string]*configschema.Attribute{ 29 "foo": {Type: cty.String, Optional: true}, 30 "bar": {Type: cty.String, Optional: true}, 31 }, 32 } 33 fooBarBlockValue := cty.ObjectVal(map[string]cty.Value{ 34 "foo": cty.StringVal("bar"), 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 "name": { 125 Type: cty.String, 126 Required: true, 127 }, 128 }, 129 }, 130 cty.ObjectVal(map[string]cty.Value{ 131 "name": cty.UnknownVal(cty.String), 132 }), 133 cty.ObjectVal(map[string]cty.Value{ 134 "name": cty.Zero, 135 }), 136 []string{ 137 `.name: wrong final value type: string required`, 138 }, 139 }, 140 { 141 &configschema.Block{ 142 Attributes: map[string]*configschema.Attribute{ 143 "name": { 144 Type: cty.String, 145 Required: true, 146 }, 147 }, 148 }, 149 cty.ObjectVal(map[string]cty.Value{ 150 "name": cty.UnknownVal(cty.String).RefineNotNull(), 151 }), 152 cty.ObjectVal(map[string]cty.Value{ 153 "name": cty.NullVal(cty.String), 154 }), 155 []string{ 156 `.name: final value cty.NullVal(cty.String) does not conform to planning placeholder cty.UnknownVal(cty.String).RefineNotNull()`, 157 }, 158 }, 159 { 160 &configschema.Block{ 161 Attributes: map[string]*configschema.Attribute{ 162 "name": { 163 Type: cty.String, 164 Required: true, 165 }, 166 }, 167 }, 168 cty.ObjectVal(map[string]cty.Value{ 169 "name": cty.UnknownVal(cty.String).Refine(). 170 StringPrefix("boop:"). 171 NewValue(), 172 }), 173 cty.ObjectVal(map[string]cty.Value{ 174 "name": cty.StringVal("thingy"), 175 }), 176 []string{ 177 `.name: final value cty.StringVal("thingy") does not conform to planning placeholder cty.UnknownVal(cty.String).Refine().StringPrefixFull("boop:").NewValue()`, 178 }, 179 }, 180 { 181 &configschema.Block{ 182 Attributes: map[string]*configschema.Attribute{ 183 "id": { 184 Type: cty.String, 185 Computed: true, 186 }, 187 "name": { 188 Type: cty.String, 189 Required: true, 190 Sensitive: true, 191 }, 192 }, 193 }, 194 cty.ObjectVal(map[string]cty.Value{ 195 "id": cty.UnknownVal(cty.String), 196 "name": cty.StringVal("wotsit"), 197 }), 198 cty.ObjectVal(map[string]cty.Value{ 199 "id": cty.UnknownVal(cty.String), 200 "name": cty.StringVal("thingy"), 201 }), 202 []string{ 203 `.name: inconsistent values for sensitive attribute`, 204 }, 205 }, 206 { 207 &configschema.Block{ 208 Attributes: map[string]*configschema.Attribute{ 209 "id": { 210 Type: cty.String, 211 Computed: true, 212 }, 213 "name": { 214 Type: cty.String, 215 Required: true, 216 }, 217 }, 218 }, 219 cty.ObjectVal(map[string]cty.Value{ 220 "id": cty.UnknownVal(cty.String), 221 "name": cty.StringVal("wotsit").Mark(marks.Sensitive), 222 }), 223 cty.ObjectVal(map[string]cty.Value{ 224 "id": cty.UnknownVal(cty.String), 225 "name": cty.StringVal("thingy"), 226 }), 227 []string{ 228 `.name: inconsistent values for sensitive attribute`, 229 }, 230 }, 231 { 232 &configschema.Block{ 233 Attributes: map[string]*configschema.Attribute{ 234 "id": { 235 Type: cty.String, 236 Computed: true, 237 }, 238 "name": { 239 Type: cty.String, 240 Required: true, 241 }, 242 }, 243 }, 244 cty.ObjectVal(map[string]cty.Value{ 245 "id": cty.UnknownVal(cty.String), 246 "name": cty.StringVal("wotsit"), 247 }), 248 cty.ObjectVal(map[string]cty.Value{ 249 "id": cty.UnknownVal(cty.String), 250 "name": cty.StringVal("thingy").Mark(marks.Sensitive), 251 }), 252 []string{ 253 `.name: inconsistent values for sensitive attribute`, 254 }, 255 }, 256 { 257 // This tests the codepath that leads to couldHaveUnknownBlockPlaceholder, 258 // where a set may be sensitive and need to be unmarked before it 259 // is iterated upon 260 &configschema.Block{ 261 BlockTypes: map[string]*configschema.NestedBlock{ 262 "configuration": { 263 Nesting: configschema.NestingList, 264 Block: configschema.Block{ 265 BlockTypes: map[string]*configschema.NestedBlock{ 266 "sensitive_fields": { 267 Nesting: configschema.NestingSet, 268 Block: schemaWithFoo, 269 }, 270 }, 271 }, 272 }, 273 }, 274 }, 275 cty.ObjectVal(map[string]cty.Value{ 276 "configuration": cty.TupleVal([]cty.Value{ 277 cty.ObjectVal(map[string]cty.Value{ 278 "sensitive_fields": cty.SetVal([]cty.Value{ 279 cty.ObjectVal(map[string]cty.Value{ 280 "foo": cty.StringVal("secret"), 281 }), 282 }).Mark(marks.Sensitive), 283 }), 284 }), 285 }), 286 cty.ObjectVal(map[string]cty.Value{ 287 "configuration": cty.TupleVal([]cty.Value{ 288 cty.ObjectVal(map[string]cty.Value{ 289 "sensitive_fields": cty.SetVal([]cty.Value{ 290 cty.ObjectVal(map[string]cty.Value{ 291 "foo": cty.StringVal("secret"), 292 }), 293 }).Mark(marks.Sensitive), 294 }), 295 }), 296 }), 297 nil, 298 }, 299 { 300 &configschema.Block{ 301 Attributes: map[string]*configschema.Attribute{ 302 "id": { 303 Type: cty.String, 304 Computed: true, 305 }, 306 "stuff": { 307 Type: cty.DynamicPseudoType, 308 Required: true, 309 }, 310 }, 311 }, 312 cty.ObjectVal(map[string]cty.Value{ 313 "id": cty.UnknownVal(cty.String), 314 "stuff": cty.DynamicVal, 315 }), 316 cty.ObjectVal(map[string]cty.Value{ 317 "id": cty.UnknownVal(cty.String), 318 "stuff": cty.StringVal("thingy"), 319 }), 320 []string{}, 321 }, 322 { 323 &configschema.Block{ 324 Attributes: map[string]*configschema.Attribute{ 325 "obj": { 326 Type: cty.Object(map[string]cty.Type{ 327 "stuff": cty.DynamicPseudoType, 328 }), 329 }, 330 }, 331 }, 332 cty.ObjectVal(map[string]cty.Value{ 333 "obj": cty.ObjectVal(map[string]cty.Value{ 334 "stuff": cty.DynamicVal, 335 }), 336 }), 337 cty.ObjectVal(map[string]cty.Value{ 338 "obj": cty.ObjectVal(map[string]cty.Value{ 339 "stuff": cty.NumberIntVal(3), 340 }), 341 }), 342 []string{}, 343 }, 344 { 345 &configschema.Block{ 346 Attributes: map[string]*configschema.Attribute{ 347 "id": { 348 Type: cty.String, 349 Computed: true, 350 }, 351 "stuff": { 352 Type: cty.DynamicPseudoType, 353 Required: true, 354 }, 355 }, 356 }, 357 cty.ObjectVal(map[string]cty.Value{ 358 "id": cty.UnknownVal(cty.String), 359 "stuff": cty.StringVal("wotsit"), 360 }), 361 cty.ObjectVal(map[string]cty.Value{ 362 "id": cty.UnknownVal(cty.String), 363 "stuff": cty.StringVal("thingy"), 364 }), 365 []string{ 366 `.stuff: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`, 367 }, 368 }, 369 { 370 &configschema.Block{ 371 Attributes: map[string]*configschema.Attribute{ 372 "id": { 373 Type: cty.String, 374 Computed: true, 375 }, 376 "stuff": { 377 Type: cty.DynamicPseudoType, 378 Required: true, 379 }, 380 }, 381 }, 382 cty.ObjectVal(map[string]cty.Value{ 383 "id": cty.UnknownVal(cty.String), 384 "stuff": cty.StringVal("true"), 385 }), 386 cty.ObjectVal(map[string]cty.Value{ 387 "id": cty.UnknownVal(cty.String), 388 "stuff": cty.True, 389 }), 390 []string{ 391 `.stuff: wrong final value type: string required`, 392 }, 393 }, 394 { 395 &configschema.Block{ 396 Attributes: map[string]*configschema.Attribute{ 397 "id": { 398 Type: cty.String, 399 Computed: true, 400 }, 401 "stuff": { 402 Type: cty.DynamicPseudoType, 403 Required: true, 404 }, 405 }, 406 }, 407 cty.ObjectVal(map[string]cty.Value{ 408 "id": cty.UnknownVal(cty.String), 409 "stuff": cty.DynamicVal, 410 }), 411 cty.ObjectVal(map[string]cty.Value{ 412 "id": cty.UnknownVal(cty.String), 413 "stuff": cty.EmptyObjectVal, 414 }), 415 nil, 416 }, 417 { 418 &configschema.Block{ 419 Attributes: map[string]*configschema.Attribute{ 420 "id": { 421 Type: cty.String, 422 Computed: true, 423 }, 424 "stuff": { 425 Type: cty.DynamicPseudoType, 426 Required: true, 427 }, 428 }, 429 }, 430 cty.ObjectVal(map[string]cty.Value{ 431 "id": cty.UnknownVal(cty.String), 432 "stuff": cty.ObjectVal(map[string]cty.Value{ 433 "nonsense": cty.StringVal("yup"), 434 }), 435 }), 436 cty.ObjectVal(map[string]cty.Value{ 437 "id": cty.UnknownVal(cty.String), 438 "stuff": cty.EmptyObjectVal, 439 }), 440 []string{ 441 `.stuff: wrong final value type: attribute "nonsense" is required`, 442 }, 443 }, 444 { 445 &configschema.Block{ 446 Attributes: map[string]*configschema.Attribute{ 447 "id": { 448 Type: cty.String, 449 Computed: true, 450 }, 451 "tags": { 452 Type: cty.Map(cty.String), 453 Optional: true, 454 }, 455 }, 456 }, 457 cty.ObjectVal(map[string]cty.Value{ 458 "id": cty.UnknownVal(cty.String), 459 "tags": cty.MapVal(map[string]cty.Value{ 460 "Name": cty.StringVal("thingy"), 461 }), 462 }), 463 cty.ObjectVal(map[string]cty.Value{ 464 "id": cty.UnknownVal(cty.String), 465 "tags": cty.MapVal(map[string]cty.Value{ 466 "Name": cty.StringVal("thingy"), 467 }), 468 }), 469 nil, 470 }, 471 { 472 &configschema.Block{ 473 Attributes: map[string]*configschema.Attribute{ 474 "id": { 475 Type: cty.String, 476 Computed: true, 477 }, 478 "tags": { 479 Type: cty.Map(cty.String), 480 Optional: true, 481 }, 482 }, 483 }, 484 cty.ObjectVal(map[string]cty.Value{ 485 "id": cty.UnknownVal(cty.String), 486 "tags": cty.MapVal(map[string]cty.Value{ 487 "Name": cty.UnknownVal(cty.String), 488 }), 489 }), 490 cty.ObjectVal(map[string]cty.Value{ 491 "id": cty.UnknownVal(cty.String), 492 "tags": cty.MapVal(map[string]cty.Value{ 493 "Name": cty.StringVal("thingy"), 494 }), 495 }), 496 nil, 497 }, 498 { 499 &configschema.Block{ 500 Attributes: map[string]*configschema.Attribute{ 501 "id": { 502 Type: cty.String, 503 Computed: true, 504 }, 505 "tags": { 506 Type: cty.Map(cty.String), 507 Optional: true, 508 }, 509 }, 510 }, 511 cty.ObjectVal(map[string]cty.Value{ 512 "id": cty.UnknownVal(cty.String), 513 "tags": cty.MapVal(map[string]cty.Value{ 514 "Name": cty.StringVal("wotsit"), 515 }), 516 }), 517 cty.ObjectVal(map[string]cty.Value{ 518 "id": cty.UnknownVal(cty.String), 519 "tags": cty.MapVal(map[string]cty.Value{ 520 "Name": cty.StringVal("thingy"), 521 }), 522 }), 523 []string{ 524 `.tags["Name"]: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`, 525 }, 526 }, 527 { 528 &configschema.Block{ 529 Attributes: map[string]*configschema.Attribute{ 530 "id": { 531 Type: cty.String, 532 Computed: true, 533 }, 534 "tags": { 535 Type: cty.Map(cty.String), 536 Optional: true, 537 }, 538 }, 539 }, 540 cty.ObjectVal(map[string]cty.Value{ 541 "id": cty.UnknownVal(cty.String), 542 "tags": cty.MapVal(map[string]cty.Value{ 543 "Name": cty.StringVal("thingy"), 544 }), 545 }), 546 cty.ObjectVal(map[string]cty.Value{ 547 "id": cty.UnknownVal(cty.String), 548 "tags": cty.MapVal(map[string]cty.Value{ 549 "Name": cty.StringVal("thingy"), 550 "Env": cty.StringVal("production"), 551 }), 552 }), 553 []string{ 554 `.tags: new element "Env" has appeared`, 555 }, 556 }, 557 { 558 &configschema.Block{ 559 Attributes: map[string]*configschema.Attribute{ 560 "id": { 561 Type: cty.String, 562 Computed: true, 563 }, 564 "tags": { 565 Type: cty.Map(cty.String), 566 Optional: true, 567 }, 568 }, 569 }, 570 cty.ObjectVal(map[string]cty.Value{ 571 "id": cty.UnknownVal(cty.String), 572 "tags": cty.MapVal(map[string]cty.Value{ 573 "Name": cty.StringVal("thingy"), 574 }), 575 }), 576 cty.ObjectVal(map[string]cty.Value{ 577 "id": cty.UnknownVal(cty.String), 578 "tags": cty.MapValEmpty(cty.String), 579 }), 580 []string{ 581 `.tags: element "Name" has vanished`, 582 }, 583 }, 584 { 585 &configschema.Block{ 586 Attributes: map[string]*configschema.Attribute{ 587 "id": { 588 Type: cty.String, 589 Computed: true, 590 }, 591 "tags": { 592 Type: cty.Map(cty.String), 593 Optional: true, 594 }, 595 }, 596 }, 597 cty.ObjectVal(map[string]cty.Value{ 598 "id": cty.UnknownVal(cty.String), 599 "tags": cty.MapVal(map[string]cty.Value{ 600 "Name": cty.UnknownVal(cty.String), 601 }), 602 }), 603 cty.ObjectVal(map[string]cty.Value{ 604 "id": cty.UnknownVal(cty.String), 605 "tags": cty.MapVal(map[string]cty.Value{ 606 "Name": cty.NullVal(cty.String), 607 }), 608 }), 609 nil, 610 }, 611 { 612 &configschema.Block{ 613 Attributes: map[string]*configschema.Attribute{ 614 "id": { 615 Type: cty.String, 616 Computed: true, 617 }, 618 "zones": { 619 Type: cty.Set(cty.String), 620 Optional: true, 621 }, 622 }, 623 }, 624 cty.ObjectVal(map[string]cty.Value{ 625 "id": cty.UnknownVal(cty.String), 626 "zones": cty.SetVal([]cty.Value{ 627 cty.StringVal("thingy"), 628 }), 629 }), 630 cty.ObjectVal(map[string]cty.Value{ 631 "id": cty.UnknownVal(cty.String), 632 "zones": cty.SetVal([]cty.Value{ 633 cty.StringVal("thingy"), 634 }), 635 }), 636 nil, 637 }, 638 { 639 &configschema.Block{ 640 Attributes: map[string]*configschema.Attribute{ 641 "id": { 642 Type: cty.String, 643 Computed: true, 644 }, 645 "zones": { 646 Type: cty.Set(cty.String), 647 Optional: true, 648 }, 649 }, 650 }, 651 cty.ObjectVal(map[string]cty.Value{ 652 "id": cty.UnknownVal(cty.String), 653 "zones": cty.SetVal([]cty.Value{ 654 cty.StringVal("thingy"), 655 }), 656 }), 657 cty.ObjectVal(map[string]cty.Value{ 658 "id": cty.UnknownVal(cty.String), 659 "zones": cty.SetVal([]cty.Value{ 660 cty.StringVal("thingy"), 661 cty.StringVal("wotsit"), 662 }), 663 }), 664 []string{ 665 `.zones: actual set element cty.StringVal("wotsit") does not correlate with any element in plan`, 666 `.zones: length changed from 1 to 2`, 667 }, 668 }, 669 { 670 &configschema.Block{ 671 Attributes: map[string]*configschema.Attribute{ 672 "id": { 673 Type: cty.String, 674 Computed: true, 675 }, 676 "zones": { 677 Type: cty.Set(cty.String), 678 Optional: true, 679 }, 680 }, 681 }, 682 cty.ObjectVal(map[string]cty.Value{ 683 "id": cty.UnknownVal(cty.String), 684 "zones": cty.SetVal([]cty.Value{ 685 cty.UnknownVal(cty.String), 686 cty.UnknownVal(cty.String), 687 }), 688 }), 689 cty.ObjectVal(map[string]cty.Value{ 690 "id": cty.UnknownVal(cty.String), 691 "zones": cty.SetVal([]cty.Value{ 692 // Imagine that both of our unknown values ultimately resolved to "thingy", 693 // causing them to collapse into a single element. That's valid, 694 // even though it's also a little confusing and counter-intuitive. 695 cty.StringVal("thingy"), 696 }), 697 }), 698 nil, 699 }, 700 { 701 &configschema.Block{ 702 Attributes: map[string]*configschema.Attribute{ 703 "id": { 704 Type: cty.String, 705 Computed: true, 706 }, 707 "names": { 708 Type: cty.List(cty.String), 709 Optional: true, 710 }, 711 }, 712 }, 713 cty.ObjectVal(map[string]cty.Value{ 714 "id": cty.UnknownVal(cty.String), 715 "names": cty.ListVal([]cty.Value{ 716 cty.StringVal("thingy"), 717 }), 718 }), 719 cty.ObjectVal(map[string]cty.Value{ 720 "id": cty.UnknownVal(cty.String), 721 "names": cty.ListVal([]cty.Value{ 722 cty.StringVal("thingy"), 723 }), 724 }), 725 nil, 726 }, 727 { 728 &configschema.Block{ 729 Attributes: map[string]*configschema.Attribute{ 730 "id": { 731 Type: cty.String, 732 Computed: true, 733 }, 734 "names": { 735 Type: cty.List(cty.String), 736 Optional: true, 737 }, 738 }, 739 }, 740 cty.ObjectVal(map[string]cty.Value{ 741 "id": cty.UnknownVal(cty.String), 742 "names": cty.UnknownVal(cty.List(cty.String)), 743 }), 744 cty.ObjectVal(map[string]cty.Value{ 745 "id": cty.UnknownVal(cty.String), 746 "names": cty.ListVal([]cty.Value{ 747 cty.StringVal("thingy"), 748 }), 749 }), 750 nil, 751 }, 752 { 753 &configschema.Block{ 754 Attributes: map[string]*configschema.Attribute{ 755 "id": { 756 Type: cty.String, 757 Computed: true, 758 }, 759 "names": { 760 Type: cty.List(cty.String), 761 Optional: true, 762 }, 763 }, 764 }, 765 cty.ObjectVal(map[string]cty.Value{ 766 "id": cty.UnknownVal(cty.String), 767 "names": cty.ListVal([]cty.Value{ 768 cty.UnknownVal(cty.String), 769 }), 770 }), 771 cty.ObjectVal(map[string]cty.Value{ 772 "id": cty.UnknownVal(cty.String), 773 "names": cty.ListVal([]cty.Value{ 774 cty.StringVal("thingy"), 775 }), 776 }), 777 nil, 778 }, 779 { 780 &configschema.Block{ 781 Attributes: map[string]*configschema.Attribute{ 782 "id": { 783 Type: cty.String, 784 Computed: true, 785 }, 786 "names": { 787 Type: cty.List(cty.String), 788 Optional: true, 789 }, 790 }, 791 }, 792 cty.ObjectVal(map[string]cty.Value{ 793 "id": cty.UnknownVal(cty.String), 794 "names": cty.ListVal([]cty.Value{ 795 cty.StringVal("thingy"), 796 cty.UnknownVal(cty.String), 797 }), 798 }), 799 cty.ObjectVal(map[string]cty.Value{ 800 "id": cty.UnknownVal(cty.String), 801 "names": cty.ListVal([]cty.Value{ 802 cty.StringVal("thingy"), 803 cty.StringVal("wotsit"), 804 }), 805 }), 806 nil, 807 }, 808 { 809 &configschema.Block{ 810 Attributes: map[string]*configschema.Attribute{ 811 "id": { 812 Type: cty.String, 813 Computed: true, 814 }, 815 "names": { 816 Type: cty.List(cty.String), 817 Optional: true, 818 }, 819 }, 820 }, 821 cty.ObjectVal(map[string]cty.Value{ 822 "id": cty.UnknownVal(cty.String), 823 "names": cty.ListVal([]cty.Value{ 824 cty.UnknownVal(cty.String), 825 cty.StringVal("thingy"), 826 }), 827 }), 828 cty.ObjectVal(map[string]cty.Value{ 829 "id": cty.UnknownVal(cty.String), 830 "names": cty.ListVal([]cty.Value{ 831 cty.StringVal("thingy"), 832 cty.StringVal("wotsit"), 833 }), 834 }), 835 []string{ 836 `.names[1]: was cty.StringVal("thingy"), but now cty.StringVal("wotsit")`, 837 }, 838 }, 839 { 840 &configschema.Block{ 841 Attributes: map[string]*configschema.Attribute{ 842 "id": { 843 Type: cty.String, 844 Computed: true, 845 }, 846 "names": { 847 Type: cty.List(cty.String), 848 Optional: true, 849 }, 850 }, 851 }, 852 cty.ObjectVal(map[string]cty.Value{ 853 "id": cty.UnknownVal(cty.String), 854 "names": cty.ListVal([]cty.Value{ 855 cty.UnknownVal(cty.String), 856 }), 857 }), 858 cty.ObjectVal(map[string]cty.Value{ 859 "id": cty.UnknownVal(cty.String), 860 "names": cty.ListVal([]cty.Value{ 861 cty.StringVal("thingy"), 862 cty.StringVal("wotsit"), 863 }), 864 }), 865 []string{ 866 `.names: new element 1 has appeared`, 867 }, 868 }, 869 870 // NestingSingle blocks 871 { 872 &configschema.Block{ 873 BlockTypes: map[string]*configschema.NestedBlock{ 874 "key": { 875 Nesting: configschema.NestingSingle, 876 Block: configschema.Block{}, 877 }, 878 }, 879 }, 880 cty.ObjectVal(map[string]cty.Value{ 881 "key": cty.EmptyObjectVal, 882 }), 883 cty.ObjectVal(map[string]cty.Value{ 884 "key": cty.EmptyObjectVal, 885 }), 886 nil, 887 }, 888 { 889 &configschema.Block{ 890 BlockTypes: map[string]*configschema.NestedBlock{ 891 "key": { 892 Nesting: configschema.NestingSingle, 893 Block: configschema.Block{}, 894 }, 895 }, 896 }, 897 cty.ObjectVal(map[string]cty.Value{ 898 "key": cty.UnknownVal(cty.EmptyObject), 899 }), 900 cty.ObjectVal(map[string]cty.Value{ 901 "key": cty.EmptyObjectVal, 902 }), 903 nil, 904 }, 905 { 906 &configschema.Block{ 907 BlockTypes: map[string]*configschema.NestedBlock{ 908 "key": { 909 Nesting: configschema.NestingSingle, 910 Block: configschema.Block{ 911 Attributes: map[string]*configschema.Attribute{ 912 "foo": { 913 Type: cty.String, 914 Optional: true, 915 }, 916 }, 917 }, 918 }, 919 }, 920 }, 921 cty.ObjectVal(map[string]cty.Value{ 922 "key": cty.NullVal(cty.Object(map[string]cty.Type{ 923 "foo": cty.String, 924 })), 925 }), 926 cty.ObjectVal(map[string]cty.Value{ 927 "key": cty.ObjectVal(map[string]cty.Value{ 928 "foo": cty.StringVal("hello"), 929 }), 930 }), 931 []string{ 932 `.key: was absent, but now present`, 933 }, 934 }, 935 { 936 &configschema.Block{ 937 BlockTypes: map[string]*configschema.NestedBlock{ 938 "key": { 939 Nesting: configschema.NestingSingle, 940 Block: configschema.Block{ 941 Attributes: map[string]*configschema.Attribute{ 942 "foo": { 943 Type: cty.String, 944 Optional: true, 945 }, 946 }, 947 }, 948 }, 949 }, 950 }, 951 cty.ObjectVal(map[string]cty.Value{ 952 "key": cty.ObjectVal(map[string]cty.Value{ 953 "foo": cty.StringVal("hello"), 954 }), 955 }), 956 cty.ObjectVal(map[string]cty.Value{ 957 "key": cty.NullVal(cty.Object(map[string]cty.Type{ 958 "foo": cty.String, 959 })), 960 }), 961 []string{ 962 `.key: was present, but now absent`, 963 }, 964 }, 965 { 966 &configschema.Block{ 967 BlockTypes: map[string]*configschema.NestedBlock{ 968 "key": { 969 Nesting: configschema.NestingSingle, 970 Block: configschema.Block{ 971 Attributes: map[string]*configschema.Attribute{ 972 "foo": { 973 Type: cty.String, 974 Optional: true, 975 }, 976 }, 977 }, 978 }, 979 }, 980 }, 981 cty.UnknownVal(cty.Object(map[string]cty.Type{ 982 "key": cty.Object(map[string]cty.Type{ 983 "foo": cty.String, 984 }), 985 })), 986 cty.ObjectVal(map[string]cty.Value{ 987 "key": cty.NullVal(cty.Object(map[string]cty.Type{ 988 "foo": cty.String, 989 })), 990 }), 991 nil, 992 }, 993 994 // NestingList blocks 995 { 996 &configschema.Block{ 997 BlockTypes: map[string]*configschema.NestedBlock{ 998 "key": { 999 Nesting: configschema.NestingList, 1000 Block: schemaWithFoo, 1001 }, 1002 }, 1003 }, 1004 cty.ObjectVal(map[string]cty.Value{ 1005 "key": cty.ListVal([]cty.Value{ 1006 fooBlockValue, 1007 }), 1008 }), 1009 cty.ObjectVal(map[string]cty.Value{ 1010 "key": cty.ListVal([]cty.Value{ 1011 fooBlockValue, 1012 }), 1013 }), 1014 nil, 1015 }, 1016 { 1017 &configschema.Block{ 1018 BlockTypes: map[string]*configschema.NestedBlock{ 1019 "key": { 1020 Nesting: configschema.NestingList, 1021 Block: schemaWithFoo, 1022 }, 1023 }, 1024 }, 1025 cty.ObjectVal(map[string]cty.Value{ 1026 "key": cty.TupleVal([]cty.Value{ 1027 fooBlockValue, 1028 fooBlockValue, 1029 }), 1030 }), 1031 cty.ObjectVal(map[string]cty.Value{ 1032 "key": cty.TupleVal([]cty.Value{ 1033 fooBlockValue, 1034 }), 1035 }), 1036 []string{ 1037 `.key: block count changed from 2 to 1`, 1038 }, 1039 }, 1040 { 1041 &configschema.Block{ 1042 BlockTypes: map[string]*configschema.NestedBlock{ 1043 "key": { 1044 Nesting: configschema.NestingList, 1045 Block: schemaWithFoo, 1046 }, 1047 }, 1048 }, 1049 cty.ObjectVal(map[string]cty.Value{ 1050 "key": cty.TupleVal([]cty.Value{}), 1051 }), 1052 cty.ObjectVal(map[string]cty.Value{ 1053 "key": cty.TupleVal([]cty.Value{ 1054 fooBlockValue, 1055 fooBlockValue, 1056 }), 1057 }), 1058 []string{ 1059 `.key: block count changed from 0 to 2`, 1060 }, 1061 }, 1062 { 1063 &configschema.Block{ 1064 BlockTypes: map[string]*configschema.NestedBlock{ 1065 "key": { 1066 Nesting: configschema.NestingList, 1067 Block: schemaWithFooBar, 1068 }, 1069 }, 1070 }, 1071 cty.UnknownVal(cty.Object(map[string]cty.Type{ 1072 "key": cty.List(fooBarBlockValue.Type()), 1073 })), 1074 cty.ObjectVal(map[string]cty.Value{ 1075 "key": cty.ListVal([]cty.Value{ 1076 cty.ObjectVal(map[string]cty.Value{ 1077 "foo": cty.StringVal("hello"), 1078 }), 1079 cty.ObjectVal(map[string]cty.Value{ 1080 "foo": cty.StringVal("world"), 1081 }), 1082 }), 1083 }), 1084 nil, // an unknown block is allowed to expand into multiple, because that's how dynamic blocks behave when for_each is unknown 1085 }, 1086 { 1087 &configschema.Block{ 1088 BlockTypes: map[string]*configschema.NestedBlock{ 1089 "key": { 1090 Nesting: configschema.NestingList, 1091 Block: schemaWithFooBar, 1092 }, 1093 }, 1094 }, 1095 // While we must make an exception for empty strings in sets due to 1096 // the legacy SDK, lists should be compared more strictly. 1097 // This does not count as a dynamic block placeholder 1098 cty.ObjectVal(map[string]cty.Value{ 1099 "key": cty.ListVal([]cty.Value{ 1100 fooBarBlockValue, 1101 cty.ObjectVal(map[string]cty.Value{ 1102 "foo": cty.UnknownVal(cty.String), 1103 "bar": cty.StringVal(""), 1104 }), 1105 }), 1106 }), 1107 cty.ObjectVal(map[string]cty.Value{ 1108 "key": cty.ListVal([]cty.Value{ 1109 fooBlockValue, 1110 cty.ObjectVal(map[string]cty.Value{ 1111 "foo": cty.StringVal("hello"), 1112 }), 1113 cty.ObjectVal(map[string]cty.Value{ 1114 "foo": cty.StringVal("world"), 1115 }), 1116 }), 1117 }), 1118 []string{".key: block count changed from 2 to 3"}, 1119 }, 1120 1121 // NestingSet blocks 1122 { 1123 &configschema.Block{ 1124 BlockTypes: map[string]*configschema.NestedBlock{ 1125 "block": { 1126 Nesting: configschema.NestingSet, 1127 Block: schemaWithFoo, 1128 }, 1129 }, 1130 }, 1131 cty.ObjectVal(map[string]cty.Value{ 1132 "block": cty.SetVal([]cty.Value{ 1133 cty.ObjectVal(map[string]cty.Value{ 1134 "foo": cty.StringVal("hello"), 1135 }), 1136 cty.ObjectVal(map[string]cty.Value{ 1137 "foo": cty.StringVal("world"), 1138 }), 1139 }), 1140 }), 1141 cty.ObjectVal(map[string]cty.Value{ 1142 "block": cty.SetVal([]cty.Value{ 1143 cty.ObjectVal(map[string]cty.Value{ 1144 "foo": cty.StringVal("hello"), 1145 }), 1146 cty.ObjectVal(map[string]cty.Value{ 1147 "foo": cty.StringVal("world"), 1148 }), 1149 }), 1150 }), 1151 nil, 1152 }, 1153 { 1154 &configschema.Block{ 1155 BlockTypes: map[string]*configschema.NestedBlock{ 1156 "block": { 1157 Nesting: configschema.NestingSet, 1158 Block: schemaWithFoo, 1159 }, 1160 }, 1161 }, 1162 cty.ObjectVal(map[string]cty.Value{ 1163 "block": cty.SetVal([]cty.Value{ 1164 cty.ObjectVal(map[string]cty.Value{ 1165 "foo": cty.UnknownVal(cty.String), 1166 }), 1167 cty.ObjectVal(map[string]cty.Value{ 1168 "foo": cty.UnknownVal(cty.String), 1169 }), 1170 }), 1171 }), 1172 cty.ObjectVal(map[string]cty.Value{ 1173 "block": cty.SetVal([]cty.Value{ 1174 // This is testing the scenario where the two unknown values 1175 // turned out to be equal after we learned their values, 1176 // and so they coalesced together into a single element. 1177 cty.ObjectVal(map[string]cty.Value{ 1178 "foo": cty.StringVal("hello"), 1179 }), 1180 }), 1181 }), 1182 nil, 1183 }, 1184 { 1185 &configschema.Block{ 1186 BlockTypes: map[string]*configschema.NestedBlock{ 1187 "block": { 1188 Nesting: configschema.NestingSet, 1189 Block: schemaWithFoo, 1190 }, 1191 }, 1192 }, 1193 cty.ObjectVal(map[string]cty.Value{ 1194 "block": cty.SetVal([]cty.Value{ 1195 cty.ObjectVal(map[string]cty.Value{ 1196 "foo": cty.UnknownVal(cty.String), 1197 }), 1198 cty.ObjectVal(map[string]cty.Value{ 1199 "foo": cty.UnknownVal(cty.String), 1200 }), 1201 }), 1202 }), 1203 cty.ObjectVal(map[string]cty.Value{ 1204 "block": cty.SetVal([]cty.Value{ 1205 cty.ObjectVal(map[string]cty.Value{ 1206 "foo": cty.StringVal("hello"), 1207 }), 1208 cty.ObjectVal(map[string]cty.Value{ 1209 "foo": cty.StringVal("world"), 1210 }), 1211 }), 1212 }), 1213 nil, 1214 }, 1215 { 1216 &configschema.Block{ 1217 BlockTypes: map[string]*configschema.NestedBlock{ 1218 "block": { 1219 Nesting: configschema.NestingSet, 1220 Block: schemaWithFoo, 1221 }, 1222 }, 1223 }, 1224 cty.ObjectVal(map[string]cty.Value{ 1225 "block": cty.UnknownVal(cty.Set( 1226 cty.Object(map[string]cty.Type{ 1227 "foo": cty.String, 1228 }), 1229 )), 1230 }), 1231 cty.ObjectVal(map[string]cty.Value{ 1232 "block": cty.SetVal([]cty.Value{ 1233 cty.ObjectVal(map[string]cty.Value{ 1234 "foo": cty.StringVal("hello"), 1235 }), 1236 cty.ObjectVal(map[string]cty.Value{ 1237 "foo": cty.StringVal("world"), 1238 }), 1239 cty.ObjectVal(map[string]cty.Value{ 1240 "foo": cty.StringVal("nope"), 1241 }), 1242 }), 1243 }), 1244 // there is no error here, because the presence of unknowns 1245 // indicates this may be a dynamic block, and the length is unknown 1246 nil, 1247 }, 1248 { 1249 &configschema.Block{ 1250 BlockTypes: map[string]*configschema.NestedBlock{ 1251 "block": { 1252 Nesting: configschema.NestingSet, 1253 Block: schemaWithFoo, 1254 }, 1255 }, 1256 }, 1257 cty.ObjectVal(map[string]cty.Value{ 1258 "block": cty.SetVal([]cty.Value{ 1259 cty.ObjectVal(map[string]cty.Value{ 1260 "foo": cty.StringVal("hello"), 1261 }), 1262 cty.ObjectVal(map[string]cty.Value{ 1263 "foo": cty.StringVal("world"), 1264 }), 1265 }), 1266 }), 1267 cty.ObjectVal(map[string]cty.Value{ 1268 "block": cty.SetVal([]cty.Value{ 1269 cty.ObjectVal(map[string]cty.Value{ 1270 "foo": cty.StringVal("howdy"), 1271 }), 1272 cty.ObjectVal(map[string]cty.Value{ 1273 "foo": cty.StringVal("world"), 1274 }), 1275 }), 1276 }), 1277 []string{ 1278 `.block: planned set element cty.ObjectVal(map[string]cty.Value{"foo":cty.StringVal("hello")}) does not correlate with any element in actual`, 1279 }, 1280 }, 1281 { 1282 // This one is an odd situation where the value representing the 1283 // block itself is unknown. This is never supposed to be true, 1284 // but in legacy SDK mode we allow such things to pass through as 1285 // a warning, and so we must tolerate them for matching purposes. 1286 &configschema.Block{ 1287 BlockTypes: map[string]*configschema.NestedBlock{ 1288 "block": { 1289 Nesting: configschema.NestingSet, 1290 Block: schemaWithFoo, 1291 }, 1292 }, 1293 }, 1294 cty.ObjectVal(map[string]cty.Value{ 1295 "block": cty.SetVal([]cty.Value{ 1296 cty.ObjectVal(map[string]cty.Value{ 1297 "foo": cty.UnknownVal(cty.String), 1298 }), 1299 cty.ObjectVal(map[string]cty.Value{ 1300 "foo": cty.UnknownVal(cty.String), 1301 }), 1302 }), 1303 }), 1304 cty.ObjectVal(map[string]cty.Value{ 1305 "block": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ 1306 "foo": cty.String, 1307 }))), 1308 }), 1309 nil, 1310 }, 1311 { 1312 &configschema.Block{ 1313 BlockTypes: map[string]*configschema.NestedBlock{ 1314 "block": { 1315 Nesting: configschema.NestingSet, 1316 Block: schemaWithFoo, 1317 }, 1318 }, 1319 }, 1320 cty.ObjectVal(map[string]cty.Value{ 1321 "block": cty.UnknownVal(cty.Set(fooBlockValue.Type())), 1322 }), 1323 cty.ObjectVal(map[string]cty.Value{ 1324 "block": cty.SetVal([]cty.Value{ 1325 cty.ObjectVal(map[string]cty.Value{ 1326 "foo": cty.StringVal("a"), 1327 }), 1328 cty.ObjectVal(map[string]cty.Value{ 1329 "foo": cty.StringVal("b"), 1330 }), 1331 }), 1332 }), 1333 nil, 1334 }, 1335 // test a set with an unknown dynamic count going to 0 values 1336 { 1337 &configschema.Block{ 1338 BlockTypes: map[string]*configschema.NestedBlock{ 1339 "block2": { 1340 Nesting: configschema.NestingSet, 1341 Block: schemaWithFoo, 1342 }, 1343 }, 1344 }, 1345 cty.ObjectVal(map[string]cty.Value{ 1346 "block2": cty.UnknownVal(cty.Set(fooBlockValue.Type())), 1347 }), 1348 cty.ObjectVal(map[string]cty.Value{ 1349 "block2": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 1350 "foo": cty.String, 1351 })), 1352 }), 1353 nil, 1354 }, 1355 // test a set with a patially known dynamic count reducing it's values 1356 { 1357 &configschema.Block{ 1358 BlockTypes: map[string]*configschema.NestedBlock{ 1359 "block3": { 1360 Nesting: configschema.NestingSet, 1361 Block: schemaWithFoo, 1362 }, 1363 }, 1364 }, 1365 cty.ObjectVal(map[string]cty.Value{ 1366 "block3": cty.SetVal([]cty.Value{ 1367 cty.ObjectVal(map[string]cty.Value{ 1368 "foo": cty.StringVal("a"), 1369 }), 1370 cty.ObjectVal(map[string]cty.Value{ 1371 "foo": cty.UnknownVal(cty.String), 1372 }), 1373 }), 1374 }), 1375 cty.ObjectVal(map[string]cty.Value{ 1376 "block3": cty.SetVal([]cty.Value{ 1377 cty.ObjectVal(map[string]cty.Value{ 1378 "foo": cty.StringVal("a"), 1379 }), 1380 }), 1381 }), 1382 nil, 1383 }, 1384 { 1385 &configschema.Block{ 1386 BlockTypes: map[string]*configschema.NestedBlock{ 1387 "block": { 1388 Nesting: configschema.NestingList, 1389 Block: configschema.Block{ 1390 Attributes: map[string]*configschema.Attribute{ 1391 "foo": { 1392 Type: cty.String, 1393 Required: true, 1394 }, 1395 }, 1396 }, 1397 }, 1398 }, 1399 }, 1400 cty.ObjectVal(map[string]cty.Value{ 1401 "block": cty.EmptyObjectVal, 1402 }), 1403 cty.ObjectVal(map[string]cty.Value{ 1404 "block": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ 1405 "foo": cty.String, 1406 }))), 1407 }), 1408 nil, 1409 }, 1410 } 1411 1412 for i, test := range tests { 1413 t.Run(fmt.Sprintf("%02d: %#v and %#v", i, test.Planned, test.Actual), func(t *testing.T) { 1414 errs := AssertObjectCompatible(test.Schema, test.Planned, test.Actual) 1415 1416 wantErrs := make(map[string]struct{}) 1417 gotErrs := make(map[string]struct{}) 1418 for _, err := range errs { 1419 gotErrs[tfdiags.FormatError(err)] = struct{}{} 1420 } 1421 for _, msg := range test.WantErrs { 1422 wantErrs[msg] = struct{}{} 1423 } 1424 1425 t.Logf("\nplanned: %sactual: %s", dump.Value(test.Planned), dump.Value(test.Actual)) 1426 for msg := range wantErrs { 1427 if _, ok := gotErrs[msg]; !ok { 1428 t.Errorf("missing expected error: %s", msg) 1429 } 1430 } 1431 for msg := range gotErrs { 1432 if _, ok := wantErrs[msg]; !ok { 1433 t.Errorf("unexpected extra error: %s", msg) 1434 } 1435 } 1436 }) 1437 } 1438 }