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