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