github.com/opentofu/opentofu@v1.7.1/internal/plans/objchange/plan_valid_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 "testing" 10 11 "github.com/apparentlymart/go-dump/dump" 12 "github.com/zclconf/go-cty/cty" 13 14 "github.com/opentofu/opentofu/internal/configs/configschema" 15 "github.com/opentofu/opentofu/internal/tfdiags" 16 ) 17 18 func TestAssertPlanValid(t *testing.T) { 19 tests := map[string]struct { 20 Schema *configschema.Block 21 Prior cty.Value 22 Config cty.Value 23 Planned cty.Value 24 WantErrs []string 25 }{ 26 "all empty": { 27 &configschema.Block{}, 28 cty.EmptyObjectVal, 29 cty.EmptyObjectVal, 30 cty.EmptyObjectVal, 31 nil, 32 }, 33 "no computed, all match": { 34 &configschema.Block{ 35 Attributes: map[string]*configschema.Attribute{ 36 "a": { 37 Type: cty.String, 38 Optional: true, 39 }, 40 }, 41 BlockTypes: map[string]*configschema.NestedBlock{ 42 "b": { 43 Nesting: configschema.NestingList, 44 Block: configschema.Block{ 45 Attributes: map[string]*configschema.Attribute{ 46 "c": { 47 Type: cty.String, 48 Optional: true, 49 }, 50 }, 51 }, 52 }, 53 }, 54 }, 55 cty.ObjectVal(map[string]cty.Value{ 56 "a": cty.StringVal("a value"), 57 "b": cty.ListVal([]cty.Value{ 58 cty.ObjectVal(map[string]cty.Value{ 59 "c": cty.StringVal("c value"), 60 }), 61 }), 62 }), 63 cty.ObjectVal(map[string]cty.Value{ 64 "a": cty.StringVal("a value"), 65 "b": cty.ListVal([]cty.Value{ 66 cty.ObjectVal(map[string]cty.Value{ 67 "c": cty.StringVal("c value"), 68 }), 69 }), 70 }), 71 cty.ObjectVal(map[string]cty.Value{ 72 "a": cty.StringVal("a value"), 73 "b": cty.ListVal([]cty.Value{ 74 cty.ObjectVal(map[string]cty.Value{ 75 "c": cty.StringVal("c value"), 76 }), 77 }), 78 }), 79 nil, 80 }, 81 "no computed, plan matches, no prior": { 82 &configschema.Block{ 83 Attributes: map[string]*configschema.Attribute{ 84 "a": { 85 Type: cty.String, 86 Optional: true, 87 }, 88 }, 89 BlockTypes: map[string]*configschema.NestedBlock{ 90 "b": { 91 Nesting: configschema.NestingList, 92 Block: configschema.Block{ 93 Attributes: map[string]*configschema.Attribute{ 94 "c": { 95 Type: cty.String, 96 Optional: true, 97 }, 98 }, 99 }, 100 }, 101 }, 102 }, 103 cty.NullVal(cty.Object(map[string]cty.Type{ 104 "a": cty.String, 105 "b": cty.List(cty.Object(map[string]cty.Type{ 106 "c": cty.String, 107 })), 108 })), 109 cty.ObjectVal(map[string]cty.Value{ 110 "a": cty.StringVal("a value"), 111 "b": cty.ListVal([]cty.Value{ 112 cty.ObjectVal(map[string]cty.Value{ 113 "c": cty.StringVal("c value"), 114 }), 115 }), 116 }), 117 cty.ObjectVal(map[string]cty.Value{ 118 "a": cty.StringVal("a value"), 119 "b": cty.ListVal([]cty.Value{ 120 cty.ObjectVal(map[string]cty.Value{ 121 "c": cty.StringVal("c value"), 122 }), 123 }), 124 }), 125 nil, 126 }, 127 "no computed, invalid change in plan": { 128 &configschema.Block{ 129 Attributes: map[string]*configschema.Attribute{ 130 "a": { 131 Type: cty.String, 132 Optional: true, 133 }, 134 }, 135 BlockTypes: map[string]*configschema.NestedBlock{ 136 "b": { 137 Nesting: configschema.NestingList, 138 Block: configschema.Block{ 139 Attributes: map[string]*configschema.Attribute{ 140 "c": { 141 Type: cty.String, 142 Optional: true, 143 }, 144 }, 145 }, 146 }, 147 }, 148 }, 149 cty.NullVal(cty.Object(map[string]cty.Type{ 150 "a": cty.String, 151 "b": cty.List(cty.Object(map[string]cty.Type{ 152 "c": cty.String, 153 })), 154 })), 155 cty.ObjectVal(map[string]cty.Value{ 156 "a": cty.StringVal("a value"), 157 "b": cty.ListVal([]cty.Value{ 158 cty.ObjectVal(map[string]cty.Value{ 159 "c": cty.StringVal("c value"), 160 }), 161 }), 162 }), 163 cty.ObjectVal(map[string]cty.Value{ 164 "a": cty.StringVal("a value"), 165 "b": cty.ListVal([]cty.Value{ 166 cty.ObjectVal(map[string]cty.Value{ 167 "c": cty.StringVal("new c value"), 168 }), 169 }), 170 }), 171 []string{ 172 `.b[0].c: planned value cty.StringVal("new c value") does not match config value cty.StringVal("c value")`, 173 }, 174 }, 175 "no computed, invalid change in plan sensitive": { 176 &configschema.Block{ 177 Attributes: map[string]*configschema.Attribute{ 178 "a": { 179 Type: cty.String, 180 Optional: true, 181 }, 182 }, 183 BlockTypes: map[string]*configschema.NestedBlock{ 184 "b": { 185 Nesting: configschema.NestingList, 186 Block: configschema.Block{ 187 Attributes: map[string]*configschema.Attribute{ 188 "c": { 189 Type: cty.String, 190 Optional: true, 191 Sensitive: true, 192 }, 193 }, 194 }, 195 }, 196 }, 197 }, 198 cty.NullVal(cty.Object(map[string]cty.Type{ 199 "a": cty.String, 200 "b": cty.List(cty.Object(map[string]cty.Type{ 201 "c": cty.String, 202 })), 203 })), 204 cty.ObjectVal(map[string]cty.Value{ 205 "a": cty.StringVal("a value"), 206 "b": cty.ListVal([]cty.Value{ 207 cty.ObjectVal(map[string]cty.Value{ 208 "c": cty.StringVal("c value"), 209 }), 210 }), 211 }), 212 cty.ObjectVal(map[string]cty.Value{ 213 "a": cty.StringVal("a value"), 214 "b": cty.ListVal([]cty.Value{ 215 cty.ObjectVal(map[string]cty.Value{ 216 "c": cty.StringVal("new c value"), 217 }), 218 }), 219 }), 220 []string{ 221 `.b[0].c: sensitive planned value does not match config value`, 222 }, 223 }, 224 "no computed, diff suppression in plan": { 225 &configschema.Block{ 226 Attributes: map[string]*configschema.Attribute{ 227 "a": { 228 Type: cty.String, 229 Optional: true, 230 }, 231 }, 232 BlockTypes: map[string]*configschema.NestedBlock{ 233 "b": { 234 Nesting: configschema.NestingList, 235 Block: configschema.Block{ 236 Attributes: map[string]*configschema.Attribute{ 237 "c": { 238 Type: cty.String, 239 Optional: true, 240 }, 241 }, 242 }, 243 }, 244 }, 245 }, 246 cty.ObjectVal(map[string]cty.Value{ 247 "a": cty.StringVal("a value"), 248 "b": cty.ListVal([]cty.Value{ 249 cty.ObjectVal(map[string]cty.Value{ 250 "c": cty.StringVal("c value"), 251 }), 252 }), 253 }), 254 cty.ObjectVal(map[string]cty.Value{ 255 "a": cty.StringVal("a value"), 256 "b": cty.ListVal([]cty.Value{ 257 cty.ObjectVal(map[string]cty.Value{ 258 "c": cty.StringVal("new c value"), 259 }), 260 }), 261 }), 262 cty.ObjectVal(map[string]cty.Value{ 263 "a": cty.StringVal("a value"), 264 "b": cty.ListVal([]cty.Value{ 265 cty.ObjectVal(map[string]cty.Value{ 266 "c": cty.StringVal("c value"), // plan uses value from prior object 267 }), 268 }), 269 }), 270 nil, 271 }, 272 "no computed, all null": { 273 &configschema.Block{ 274 Attributes: map[string]*configschema.Attribute{ 275 "a": { 276 Type: cty.String, 277 Optional: true, 278 }, 279 }, 280 BlockTypes: map[string]*configschema.NestedBlock{ 281 "b": { 282 Nesting: configschema.NestingList, 283 Block: configschema.Block{ 284 Attributes: map[string]*configschema.Attribute{ 285 "c": { 286 Type: cty.String, 287 Optional: true, 288 }, 289 }, 290 }, 291 }, 292 }, 293 }, 294 cty.ObjectVal(map[string]cty.Value{ 295 "a": cty.NullVal(cty.String), 296 "b": cty.ListVal([]cty.Value{ 297 cty.ObjectVal(map[string]cty.Value{ 298 "c": cty.NullVal(cty.String), 299 }), 300 }), 301 }), 302 cty.ObjectVal(map[string]cty.Value{ 303 "a": cty.NullVal(cty.String), 304 "b": cty.ListVal([]cty.Value{ 305 cty.ObjectVal(map[string]cty.Value{ 306 "c": cty.NullVal(cty.String), 307 }), 308 }), 309 }), 310 cty.ObjectVal(map[string]cty.Value{ 311 "a": cty.NullVal(cty.String), 312 "b": cty.ListVal([]cty.Value{ 313 cty.ObjectVal(map[string]cty.Value{ 314 "c": cty.NullVal(cty.String), 315 }), 316 }), 317 }), 318 nil, 319 }, 320 "nested map, normal update": { 321 &configschema.Block{ 322 BlockTypes: map[string]*configschema.NestedBlock{ 323 "b": { 324 Nesting: configschema.NestingMap, 325 Block: configschema.Block{ 326 Attributes: map[string]*configschema.Attribute{ 327 "c": { 328 Type: cty.String, 329 Optional: true, 330 }, 331 }, 332 }, 333 }, 334 }, 335 }, 336 cty.ObjectVal(map[string]cty.Value{ 337 "b": cty.MapVal(map[string]cty.Value{ 338 "boop": cty.ObjectVal(map[string]cty.Value{ 339 "c": cty.StringVal("hello"), 340 }), 341 }), 342 }), 343 cty.ObjectVal(map[string]cty.Value{ 344 "b": cty.MapVal(map[string]cty.Value{ 345 "boop": cty.ObjectVal(map[string]cty.Value{ 346 "c": cty.StringVal("howdy"), 347 }), 348 }), 349 }), 350 cty.ObjectVal(map[string]cty.Value{ 351 "b": cty.MapVal(map[string]cty.Value{ 352 "boop": cty.ObjectVal(map[string]cty.Value{ 353 "c": cty.StringVal("howdy"), 354 }), 355 }), 356 }), 357 nil, 358 }, 359 360 // Nested block collections are never null 361 "nested list, null in plan": { 362 &configschema.Block{ 363 BlockTypes: map[string]*configschema.NestedBlock{ 364 "b": { 365 Nesting: configschema.NestingList, 366 Block: configschema.Block{ 367 Attributes: map[string]*configschema.Attribute{ 368 "c": { 369 Type: cty.String, 370 Optional: true, 371 }, 372 }, 373 }, 374 }, 375 }, 376 }, 377 cty.NullVal(cty.Object(map[string]cty.Type{ 378 "b": cty.List(cty.Object(map[string]cty.Type{ 379 "c": cty.String, 380 })), 381 })), 382 cty.ObjectVal(map[string]cty.Value{ 383 "b": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 384 "c": cty.String, 385 })), 386 }), 387 cty.ObjectVal(map[string]cty.Value{ 388 "b": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 389 "c": cty.String, 390 }))), 391 }), 392 []string{ 393 `.b: attribute representing a list of nested blocks must be empty to indicate no blocks, not null`, 394 }, 395 }, 396 397 // but don't panic on a null list just in case 398 "nested list, null in config": { 399 &configschema.Block{ 400 BlockTypes: map[string]*configschema.NestedBlock{ 401 "b": { 402 Nesting: configschema.NestingList, 403 Block: configschema.Block{ 404 Attributes: map[string]*configschema.Attribute{ 405 "c": { 406 Type: cty.String, 407 Optional: true, 408 }, 409 }, 410 }, 411 }, 412 }, 413 }, 414 cty.ObjectVal(map[string]cty.Value{ 415 "b": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 416 "c": cty.String, 417 })), 418 }), 419 cty.ObjectVal(map[string]cty.Value{ 420 "b": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 421 "c": cty.String, 422 }))), 423 }), 424 cty.ObjectVal(map[string]cty.Value{ 425 "b": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 426 "c": cty.String, 427 })), 428 }), 429 nil, 430 }, 431 432 // blocks can be unknown when using dynamic 433 "nested list, unknown nested dynamic": { 434 &configschema.Block{ 435 BlockTypes: map[string]*configschema.NestedBlock{ 436 "a": { 437 Nesting: configschema.NestingList, 438 Block: configschema.Block{ 439 BlockTypes: map[string]*configschema.NestedBlock{ 440 "b": { 441 Nesting: configschema.NestingList, 442 Block: configschema.Block{ 443 Attributes: map[string]*configschema.Attribute{ 444 "c": { 445 Type: cty.String, 446 Optional: true, 447 }, 448 "computed": { 449 Type: cty.String, 450 Computed: true, 451 }, 452 }, 453 }, 454 }, 455 }, 456 }, 457 }, 458 }, 459 }, 460 461 cty.ObjectVal(map[string]cty.Value{ 462 "a": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 463 "computed": cty.NullVal(cty.String), 464 "b": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 465 "c": cty.StringVal("x"), 466 })}), 467 })}), 468 }), 469 cty.ObjectVal(map[string]cty.Value{ 470 "a": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 471 "b": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ 472 "c": cty.String, 473 "computed": cty.String, 474 }))), 475 })}), 476 }), 477 cty.ObjectVal(map[string]cty.Value{ 478 "a": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 479 "b": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ 480 "c": cty.String, 481 "computed": cty.String, 482 }))), 483 })}), 484 }), 485 []string{}, 486 }, 487 488 "nested set, unknown dynamic cannot be planned": { 489 &configschema.Block{ 490 Attributes: map[string]*configschema.Attribute{ 491 "computed": { 492 Type: cty.String, 493 Computed: true, 494 }, 495 }, 496 BlockTypes: map[string]*configschema.NestedBlock{ 497 "b": { 498 Nesting: configschema.NestingSet, 499 Block: configschema.Block{ 500 Attributes: map[string]*configschema.Attribute{ 501 "c": { 502 Type: cty.String, 503 Optional: true, 504 }, 505 }, 506 }, 507 }, 508 }, 509 }, 510 511 cty.ObjectVal(map[string]cty.Value{ 512 "computed": cty.NullVal(cty.String), 513 "b": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 514 "c": cty.StringVal("x"), 515 })}), 516 }), 517 cty.ObjectVal(map[string]cty.Value{ 518 "computed": cty.NullVal(cty.String), 519 "b": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ 520 "c": cty.String, 521 }))), 522 }), 523 cty.ObjectVal(map[string]cty.Value{ 524 "computed": cty.StringVal("default"), 525 "b": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 526 "c": cty.StringVal("oops"), 527 })}), 528 }), 529 530 []string{ 531 `.b: planned value cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"c":cty.StringVal("oops")})}) for unknown dynamic block`, 532 }, 533 }, 534 535 "nested set, null in plan": { 536 &configschema.Block{ 537 BlockTypes: map[string]*configschema.NestedBlock{ 538 "b": { 539 Nesting: configschema.NestingSet, 540 Block: configschema.Block{ 541 Attributes: map[string]*configschema.Attribute{ 542 "c": { 543 Type: cty.String, 544 Optional: true, 545 }, 546 }, 547 }, 548 }, 549 }, 550 }, 551 cty.NullVal(cty.Object(map[string]cty.Type{ 552 "b": cty.Set(cty.Object(map[string]cty.Type{ 553 "c": cty.String, 554 })), 555 })), 556 cty.ObjectVal(map[string]cty.Value{ 557 "b": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 558 "c": cty.String, 559 })), 560 }), 561 cty.ObjectVal(map[string]cty.Value{ 562 "b": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 563 "c": cty.String, 564 }))), 565 }), 566 []string{ 567 `.b: attribute representing a set of nested blocks must be empty to indicate no blocks, not null`, 568 }, 569 }, 570 "nested map, null in plan": { 571 &configschema.Block{ 572 BlockTypes: map[string]*configschema.NestedBlock{ 573 "b": { 574 Nesting: configschema.NestingMap, 575 Block: configschema.Block{ 576 Attributes: map[string]*configschema.Attribute{ 577 "c": { 578 Type: cty.String, 579 Optional: true, 580 }, 581 }, 582 }, 583 }, 584 }, 585 }, 586 cty.NullVal(cty.Object(map[string]cty.Type{ 587 "b": cty.Map(cty.Object(map[string]cty.Type{ 588 "c": cty.String, 589 })), 590 })), 591 cty.ObjectVal(map[string]cty.Value{ 592 "b": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 593 "c": cty.String, 594 })), 595 }), 596 cty.ObjectVal(map[string]cty.Value{ 597 "b": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ 598 "c": cty.String, 599 }))), 600 }), 601 []string{ 602 `.b: attribute representing a map of nested blocks must be empty to indicate no blocks, not null`, 603 }, 604 }, 605 606 // We don't actually do any validation for nested set blocks, and so 607 // the remaining cases here are just intending to ensure we don't 608 // inadvertently start generating errors incorrectly in future. 609 "nested set, no computed, no changes": { 610 &configschema.Block{ 611 BlockTypes: map[string]*configschema.NestedBlock{ 612 "b": { 613 Nesting: configschema.NestingSet, 614 Block: configschema.Block{ 615 Attributes: map[string]*configschema.Attribute{ 616 "c": { 617 Type: cty.String, 618 Optional: true, 619 }, 620 }, 621 }, 622 }, 623 }, 624 }, 625 cty.ObjectVal(map[string]cty.Value{ 626 "b": cty.SetVal([]cty.Value{ 627 cty.ObjectVal(map[string]cty.Value{ 628 "c": cty.StringVal("c value"), 629 }), 630 }), 631 }), 632 cty.ObjectVal(map[string]cty.Value{ 633 "b": cty.SetVal([]cty.Value{ 634 cty.ObjectVal(map[string]cty.Value{ 635 "c": cty.StringVal("c value"), 636 }), 637 }), 638 }), 639 cty.ObjectVal(map[string]cty.Value{ 640 "b": cty.SetVal([]cty.Value{ 641 cty.ObjectVal(map[string]cty.Value{ 642 "c": cty.StringVal("c value"), 643 }), 644 }), 645 }), 646 nil, 647 }, 648 "nested set, no computed, invalid change in plan": { 649 &configschema.Block{ 650 BlockTypes: map[string]*configschema.NestedBlock{ 651 "b": { 652 Nesting: configschema.NestingSet, 653 Block: configschema.Block{ 654 Attributes: map[string]*configschema.Attribute{ 655 "c": { 656 Type: cty.String, 657 Optional: true, 658 }, 659 }, 660 }, 661 }, 662 }, 663 }, 664 cty.ObjectVal(map[string]cty.Value{ 665 "b": cty.SetVal([]cty.Value{ 666 cty.ObjectVal(map[string]cty.Value{ 667 "c": cty.StringVal("c value"), 668 }), 669 }), 670 }), 671 cty.ObjectVal(map[string]cty.Value{ 672 "b": cty.SetVal([]cty.Value{ 673 cty.ObjectVal(map[string]cty.Value{ 674 "c": cty.StringVal("c value"), 675 }), 676 }), 677 }), 678 cty.ObjectVal(map[string]cty.Value{ 679 "b": cty.SetVal([]cty.Value{ 680 cty.ObjectVal(map[string]cty.Value{ 681 "c": cty.StringVal("new c value"), // matches neither prior nor config 682 }), 683 }), 684 }), 685 nil, 686 }, 687 "nested set, no computed, diff suppressed": { 688 &configschema.Block{ 689 BlockTypes: map[string]*configschema.NestedBlock{ 690 "b": { 691 Nesting: configschema.NestingSet, 692 Block: configschema.Block{ 693 Attributes: map[string]*configschema.Attribute{ 694 "c": { 695 Type: cty.String, 696 Optional: true, 697 }, 698 }, 699 }, 700 }, 701 }, 702 }, 703 cty.ObjectVal(map[string]cty.Value{ 704 "b": cty.SetVal([]cty.Value{ 705 cty.ObjectVal(map[string]cty.Value{ 706 "c": cty.StringVal("c value"), 707 }), 708 }), 709 }), 710 cty.ObjectVal(map[string]cty.Value{ 711 "b": cty.SetVal([]cty.Value{ 712 cty.ObjectVal(map[string]cty.Value{ 713 "c": cty.StringVal("new c value"), 714 }), 715 }), 716 }), 717 cty.ObjectVal(map[string]cty.Value{ 718 "b": cty.SetVal([]cty.Value{ 719 cty.ObjectVal(map[string]cty.Value{ 720 "c": cty.StringVal("c value"), // plan uses value from prior object 721 }), 722 }), 723 }), 724 nil, 725 }, 726 727 // Attributes with NestedTypes 728 "NestedType attr, no computed, all match": { 729 &configschema.Block{ 730 Attributes: map[string]*configschema.Attribute{ 731 "a": { 732 NestedType: &configschema.Object{ 733 Nesting: configschema.NestingList, 734 Attributes: map[string]*configschema.Attribute{ 735 "b": { 736 Type: cty.String, 737 Optional: true, 738 }, 739 }, 740 }, 741 Optional: true, 742 }, 743 }, 744 }, 745 cty.ObjectVal(map[string]cty.Value{ 746 "a": cty.ListVal([]cty.Value{ 747 cty.ObjectVal(map[string]cty.Value{ 748 "b": cty.StringVal("b value"), 749 }), 750 }), 751 }), 752 cty.ObjectVal(map[string]cty.Value{ 753 "a": cty.ListVal([]cty.Value{ 754 cty.ObjectVal(map[string]cty.Value{ 755 "b": cty.StringVal("b value"), 756 }), 757 }), 758 }), 759 cty.ObjectVal(map[string]cty.Value{ 760 "a": cty.ListVal([]cty.Value{ 761 cty.ObjectVal(map[string]cty.Value{ 762 "b": cty.StringVal("b value"), 763 }), 764 }), 765 }), 766 nil, 767 }, 768 "NestedType attr, no computed, plan matches, no prior": { 769 &configschema.Block{ 770 Attributes: map[string]*configschema.Attribute{ 771 "a": { 772 NestedType: &configschema.Object{ 773 Nesting: configschema.NestingList, 774 Attributes: map[string]*configschema.Attribute{ 775 "b": { 776 Type: cty.String, 777 Optional: true, 778 }, 779 }, 780 }, 781 Optional: true, 782 }, 783 }, 784 }, 785 cty.NullVal(cty.Object(map[string]cty.Type{ 786 "a": cty.List(cty.Object(map[string]cty.Type{ 787 "b": cty.String, 788 })), 789 })), 790 cty.ObjectVal(map[string]cty.Value{ 791 "a": cty.ListVal([]cty.Value{ 792 cty.ObjectVal(map[string]cty.Value{ 793 "b": cty.StringVal("c value"), 794 }), 795 }), 796 }), 797 cty.ObjectVal(map[string]cty.Value{ 798 "a": cty.ListVal([]cty.Value{ 799 cty.ObjectVal(map[string]cty.Value{ 800 "b": cty.StringVal("c value"), 801 }), 802 }), 803 }), 804 nil, 805 }, 806 "NestedType, no computed, invalid change in plan": { 807 &configschema.Block{ 808 Attributes: map[string]*configschema.Attribute{ 809 "a": { 810 NestedType: &configschema.Object{ 811 Nesting: configschema.NestingList, 812 Attributes: map[string]*configschema.Attribute{ 813 "b": { 814 Type: cty.String, 815 Optional: true, 816 }, 817 }, 818 }, 819 Optional: true, 820 }, 821 }, 822 }, 823 cty.NullVal(cty.Object(map[string]cty.Type{ 824 "a": cty.List(cty.Object(map[string]cty.Type{ 825 "b": cty.String, 826 })), 827 })), 828 cty.ObjectVal(map[string]cty.Value{ 829 "a": cty.ListVal([]cty.Value{ 830 cty.ObjectVal(map[string]cty.Value{ 831 "b": cty.StringVal("c value"), 832 }), 833 }), 834 }), 835 cty.ObjectVal(map[string]cty.Value{ 836 "a": cty.ListVal([]cty.Value{ 837 cty.ObjectVal(map[string]cty.Value{ 838 "b": cty.StringVal("new c value"), 839 }), 840 }), 841 }), 842 []string{ 843 `.a[0].b: planned value cty.StringVal("new c value") does not match config value cty.StringVal("c value")`, 844 }, 845 }, 846 "NestedType attr, no computed, invalid change in plan sensitive": { 847 &configschema.Block{ 848 Attributes: map[string]*configschema.Attribute{ 849 "a": { 850 NestedType: &configschema.Object{ 851 Nesting: configschema.NestingList, 852 Attributes: map[string]*configschema.Attribute{ 853 "b": { 854 Type: cty.String, 855 Optional: true, 856 Sensitive: true, 857 }, 858 }, 859 }, 860 Optional: true, 861 }, 862 }, 863 }, 864 cty.NullVal(cty.Object(map[string]cty.Type{ 865 "a": cty.List(cty.Object(map[string]cty.Type{ 866 "b": cty.String, 867 })), 868 })), 869 cty.ObjectVal(map[string]cty.Value{ 870 "a": cty.ListVal([]cty.Value{ 871 cty.ObjectVal(map[string]cty.Value{ 872 "b": cty.StringVal("b value"), 873 }), 874 }), 875 }), 876 cty.ObjectVal(map[string]cty.Value{ 877 "a": cty.ListVal([]cty.Value{ 878 cty.ObjectVal(map[string]cty.Value{ 879 "b": cty.StringVal("new b value"), 880 }), 881 }), 882 }), 883 []string{ 884 `.a[0].b: sensitive planned value does not match config value`, 885 }, 886 }, 887 "NestedType attr, no computed, diff suppression in plan": { 888 &configschema.Block{ 889 Attributes: map[string]*configschema.Attribute{ 890 "a": { 891 NestedType: &configschema.Object{ 892 Nesting: configschema.NestingList, 893 Attributes: map[string]*configschema.Attribute{ 894 "b": { 895 Type: cty.String, 896 Optional: true, 897 }, 898 }, 899 }, 900 Optional: true, 901 }, 902 }, 903 }, 904 cty.ObjectVal(map[string]cty.Value{ 905 "a": cty.ListVal([]cty.Value{ 906 cty.ObjectVal(map[string]cty.Value{ 907 "b": cty.StringVal("b value"), 908 }), 909 }), 910 }), 911 cty.ObjectVal(map[string]cty.Value{ 912 "a": cty.ListVal([]cty.Value{ 913 cty.ObjectVal(map[string]cty.Value{ 914 "b": cty.StringVal("new b value"), 915 }), 916 }), 917 }), 918 cty.ObjectVal(map[string]cty.Value{ 919 "a": cty.ListVal([]cty.Value{ 920 cty.ObjectVal(map[string]cty.Value{ 921 "b": cty.StringVal("b value"), // plan uses value from prior object 922 }), 923 }), 924 }), 925 nil, 926 }, 927 "NestedType attr, no computed, all null": { 928 &configschema.Block{ 929 Attributes: map[string]*configschema.Attribute{ 930 "a": { 931 NestedType: &configschema.Object{ 932 Nesting: configschema.NestingList, 933 Attributes: map[string]*configschema.Attribute{ 934 "b": { 935 Type: cty.String, 936 Optional: true, 937 }, 938 }, 939 }, 940 Optional: true, 941 }, 942 }, 943 }, 944 cty.ObjectVal(map[string]cty.Value{ 945 "a": cty.NullVal(cty.DynamicPseudoType), 946 }), 947 cty.ObjectVal(map[string]cty.Value{ 948 "a": cty.NullVal(cty.DynamicPseudoType), 949 }), 950 cty.ObjectVal(map[string]cty.Value{ 951 "a": cty.NullVal(cty.DynamicPseudoType), 952 }), 953 nil, 954 }, 955 "NestedType attr, no computed, all zero value": { 956 &configschema.Block{ 957 Attributes: map[string]*configschema.Attribute{ 958 "a": { 959 NestedType: &configschema.Object{ 960 Nesting: configschema.NestingList, 961 Attributes: map[string]*configschema.Attribute{ 962 "b": { 963 Type: cty.String, 964 Optional: true, 965 }, 966 }, 967 }, 968 Optional: true, 969 }, 970 }, 971 }, 972 cty.ObjectVal(map[string]cty.Value{ 973 "a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 974 "b": cty.String, 975 }))), 976 }), 977 cty.ObjectVal(map[string]cty.Value{ 978 "a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 979 "b": cty.String, 980 }))), 981 }), 982 cty.ObjectVal(map[string]cty.Value{ 983 "a": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 984 "b": cty.String, 985 }))), 986 }), 987 nil, 988 }, 989 "NestedType NestingSet attribute to null": { 990 &configschema.Block{ 991 Attributes: map[string]*configschema.Attribute{ 992 "bloop": { 993 NestedType: &configschema.Object{ 994 Nesting: configschema.NestingSet, 995 Attributes: map[string]*configschema.Attribute{ 996 "blop": { 997 Type: cty.String, 998 Required: true, 999 }, 1000 }, 1001 }, 1002 Optional: true, 1003 }, 1004 }, 1005 }, 1006 cty.ObjectVal(map[string]cty.Value{ 1007 "bloop": cty.SetVal([]cty.Value{ 1008 cty.ObjectVal(map[string]cty.Value{ 1009 "blop": cty.StringVal("ok"), 1010 }), 1011 }), 1012 }), 1013 cty.ObjectVal(map[string]cty.Value{ 1014 "bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 1015 "blop": cty.String, 1016 }))), 1017 }), 1018 cty.ObjectVal(map[string]cty.Value{ 1019 "bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 1020 "blop": cty.String, 1021 }))), 1022 }), 1023 nil, 1024 }, 1025 "NestedType deep nested optional set attribute to null": { 1026 &configschema.Block{ 1027 Attributes: map[string]*configschema.Attribute{ 1028 "bleep": { 1029 NestedType: &configschema.Object{ 1030 Nesting: configschema.NestingList, 1031 Attributes: map[string]*configschema.Attribute{ 1032 "bloop": { 1033 NestedType: &configschema.Object{ 1034 Nesting: configschema.NestingSet, 1035 Attributes: map[string]*configschema.Attribute{ 1036 "blome": { 1037 Type: cty.String, 1038 Optional: true, 1039 }, 1040 }, 1041 }, 1042 Optional: true, 1043 }, 1044 }, 1045 }, 1046 Optional: true, 1047 }, 1048 }, 1049 }, 1050 cty.ObjectVal(map[string]cty.Value{ 1051 "bleep": cty.ListVal([]cty.Value{ 1052 cty.ObjectVal(map[string]cty.Value{ 1053 "bloop": cty.SetVal([]cty.Value{ 1054 cty.ObjectVal(map[string]cty.Value{ 1055 "blome": cty.StringVal("ok"), 1056 }), 1057 }), 1058 }), 1059 }), 1060 }), 1061 cty.ObjectVal(map[string]cty.Value{ 1062 "bleep": cty.ListVal([]cty.Value{ 1063 cty.ObjectVal(map[string]cty.Value{ 1064 "bloop": cty.NullVal(cty.Set( 1065 cty.Object(map[string]cty.Type{ 1066 "blome": cty.String, 1067 }), 1068 )), 1069 }), 1070 }), 1071 }), 1072 cty.ObjectVal(map[string]cty.Value{ 1073 "bleep": cty.ListVal([]cty.Value{ 1074 cty.ObjectVal(map[string]cty.Value{ 1075 "bloop": cty.NullVal(cty.List( 1076 cty.Object(map[string]cty.Type{ 1077 "blome": cty.String, 1078 }), 1079 )), 1080 }), 1081 }), 1082 }), 1083 nil, 1084 }, 1085 "NestedType deep nested set": { 1086 &configschema.Block{ 1087 Attributes: map[string]*configschema.Attribute{ 1088 "bleep": { 1089 NestedType: &configschema.Object{ 1090 Nesting: configschema.NestingList, 1091 Attributes: map[string]*configschema.Attribute{ 1092 "bloop": { 1093 NestedType: &configschema.Object{ 1094 Nesting: configschema.NestingSet, 1095 Attributes: map[string]*configschema.Attribute{ 1096 "blome": { 1097 Type: cty.String, 1098 Optional: true, 1099 }, 1100 }, 1101 }, 1102 Optional: true, 1103 }, 1104 }, 1105 }, 1106 Optional: true, 1107 }, 1108 }, 1109 }, 1110 cty.ObjectVal(map[string]cty.Value{ 1111 "bleep": cty.ListVal([]cty.Value{ 1112 cty.ObjectVal(map[string]cty.Value{ 1113 "bloop": cty.SetVal([]cty.Value{ 1114 cty.ObjectVal(map[string]cty.Value{ 1115 "blome": cty.StringVal("ok"), 1116 }), 1117 }), 1118 }), 1119 }), 1120 }), 1121 // Note: bloop is null in the config 1122 cty.ObjectVal(map[string]cty.Value{ 1123 "bleep": cty.ListVal([]cty.Value{ 1124 cty.ObjectVal(map[string]cty.Value{ 1125 "bloop": cty.NullVal(cty.Set( 1126 cty.Object(map[string]cty.Type{ 1127 "blome": cty.String, 1128 }), 1129 )), 1130 }), 1131 }), 1132 }), 1133 // provider sends back the prior value, not matching the config 1134 cty.ObjectVal(map[string]cty.Value{ 1135 "bleep": cty.ListVal([]cty.Value{ 1136 cty.ObjectVal(map[string]cty.Value{ 1137 "bloop": cty.SetVal([]cty.Value{ 1138 cty.ObjectVal(map[string]cty.Value{ 1139 "blome": cty.StringVal("ok"), 1140 }), 1141 }), 1142 }), 1143 }), 1144 }), 1145 nil, // we cannot validate individual set elements, and trust the provider's response 1146 }, 1147 "NestedType nested computed list attribute": { 1148 &configschema.Block{ 1149 Attributes: map[string]*configschema.Attribute{ 1150 "bloop": { 1151 NestedType: &configschema.Object{ 1152 Nesting: configschema.NestingList, 1153 Attributes: map[string]*configschema.Attribute{ 1154 "blop": { 1155 Type: cty.String, 1156 Optional: true, 1157 }, 1158 }, 1159 }, 1160 Computed: true, 1161 }, 1162 }, 1163 }, 1164 cty.ObjectVal(map[string]cty.Value{ 1165 "bloop": cty.ListVal([]cty.Value{ 1166 cty.ObjectVal(map[string]cty.Value{ 1167 "blop": cty.StringVal("ok"), 1168 }), 1169 }), 1170 }), 1171 cty.ObjectVal(map[string]cty.Value{ 1172 "bloop": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 1173 "blop": cty.String, 1174 }))), 1175 }), 1176 1177 cty.ObjectVal(map[string]cty.Value{ 1178 "bloop": cty.ListVal([]cty.Value{ 1179 cty.ObjectVal(map[string]cty.Value{ 1180 "blop": cty.StringVal("ok"), 1181 }), 1182 }), 1183 }), 1184 nil, 1185 }, 1186 "NestedType nested list attribute to null": { 1187 &configschema.Block{ 1188 Attributes: map[string]*configschema.Attribute{ 1189 "bloop": { 1190 NestedType: &configschema.Object{ 1191 Nesting: configschema.NestingList, 1192 Attributes: map[string]*configschema.Attribute{ 1193 "blop": { 1194 Type: cty.String, 1195 Optional: true, 1196 }, 1197 }, 1198 }, 1199 Optional: true, 1200 }, 1201 }, 1202 }, 1203 cty.ObjectVal(map[string]cty.Value{ 1204 "bloop": cty.ListVal([]cty.Value{ 1205 cty.ObjectVal(map[string]cty.Value{ 1206 "blop": cty.StringVal("ok"), 1207 }), 1208 }), 1209 }), 1210 cty.ObjectVal(map[string]cty.Value{ 1211 "bloop": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 1212 "blop": cty.String, 1213 }))), 1214 }), 1215 1216 // provider returned the old value 1217 cty.ObjectVal(map[string]cty.Value{ 1218 "bloop": cty.ListVal([]cty.Value{ 1219 cty.ObjectVal(map[string]cty.Value{ 1220 "blop": cty.StringVal("ok"), 1221 }), 1222 }), 1223 }), 1224 []string{`.bloop: planned value cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"blop":cty.StringVal("ok")})}) for a non-computed attribute`}, 1225 }, 1226 "NestedType nested set attribute to null": { 1227 &configschema.Block{ 1228 Attributes: map[string]*configschema.Attribute{ 1229 "bloop": { 1230 NestedType: &configschema.Object{ 1231 Nesting: configschema.NestingSet, 1232 Attributes: map[string]*configschema.Attribute{ 1233 "blop": { 1234 Type: cty.String, 1235 Optional: true, 1236 }, 1237 }, 1238 }, 1239 Optional: true, 1240 }, 1241 }, 1242 }, 1243 cty.ObjectVal(map[string]cty.Value{ 1244 "bloop": cty.SetVal([]cty.Value{ 1245 cty.ObjectVal(map[string]cty.Value{ 1246 "blop": cty.StringVal("ok"), 1247 }), 1248 }), 1249 }), 1250 cty.ObjectVal(map[string]cty.Value{ 1251 "bloop": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 1252 "blop": cty.String, 1253 }))), 1254 }), 1255 // provider returned the old value 1256 cty.ObjectVal(map[string]cty.Value{ 1257 "bloop": cty.ListVal([]cty.Value{ 1258 cty.ObjectVal(map[string]cty.Value{ 1259 "blop": cty.StringVal("ok"), 1260 }), 1261 }), 1262 }), 1263 []string{`.bloop: planned value cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{"blop":cty.StringVal("ok")})}) for a non-computed attribute`}, 1264 }, 1265 "computed within nested objects": { 1266 &configschema.Block{ 1267 Attributes: map[string]*configschema.Attribute{ 1268 "map": { 1269 NestedType: &configschema.Object{ 1270 Nesting: configschema.NestingMap, 1271 Attributes: map[string]*configschema.Attribute{ 1272 "name": { 1273 Type: cty.String, 1274 Computed: true, 1275 }, 1276 }, 1277 }, 1278 }, 1279 // When an object has dynamic attrs, the map may be 1280 // handled as an object. 1281 "map_as_obj": { 1282 NestedType: &configschema.Object{ 1283 Nesting: configschema.NestingMap, 1284 Attributes: map[string]*configschema.Attribute{ 1285 "name": { 1286 Type: cty.String, 1287 Computed: true, 1288 }, 1289 }, 1290 }, 1291 }, 1292 "list": { 1293 NestedType: &configschema.Object{ 1294 Nesting: configschema.NestingList, 1295 Attributes: map[string]*configschema.Attribute{ 1296 "name": { 1297 Type: cty.String, 1298 Computed: true, 1299 }, 1300 }, 1301 }, 1302 }, 1303 "set": { 1304 NestedType: &configschema.Object{ 1305 Nesting: configschema.NestingSet, 1306 Attributes: map[string]*configschema.Attribute{ 1307 "name": { 1308 Type: cty.String, 1309 Computed: true, 1310 }, 1311 }, 1312 }, 1313 }, 1314 "single": { 1315 NestedType: &configschema.Object{ 1316 Nesting: configschema.NestingSingle, 1317 Attributes: map[string]*configschema.Attribute{ 1318 "name": { 1319 Type: cty.DynamicPseudoType, 1320 Computed: true, 1321 }, 1322 }, 1323 }, 1324 }, 1325 }, 1326 }, 1327 cty.NullVal(cty.Object(map[string]cty.Type{ 1328 "map": cty.Map(cty.Object(map[string]cty.Type{ 1329 "name": cty.String, 1330 })), 1331 "map_as_obj": cty.Map(cty.Object(map[string]cty.Type{ 1332 "name": cty.DynamicPseudoType, 1333 })), 1334 "list": cty.List(cty.Object(map[string]cty.Type{ 1335 "name": cty.String, 1336 })), 1337 "set": cty.Set(cty.Object(map[string]cty.Type{ 1338 "name": cty.String, 1339 })), 1340 "single": cty.Object(map[string]cty.Type{ 1341 "name": cty.String, 1342 }), 1343 })), 1344 cty.ObjectVal(map[string]cty.Value{ 1345 "map": cty.MapVal(map[string]cty.Value{ 1346 "one": cty.ObjectVal(map[string]cty.Value{ 1347 "name": cty.NullVal(cty.String), 1348 }), 1349 }), 1350 "map_as_obj": cty.MapVal(map[string]cty.Value{ 1351 "one": cty.ObjectVal(map[string]cty.Value{ 1352 "name": cty.NullVal(cty.DynamicPseudoType), 1353 }), 1354 }), 1355 "list": cty.ListVal([]cty.Value{ 1356 cty.ObjectVal(map[string]cty.Value{ 1357 "name": cty.NullVal(cty.String), 1358 }), 1359 }), 1360 "set": cty.SetVal([]cty.Value{ 1361 cty.ObjectVal(map[string]cty.Value{ 1362 "name": cty.NullVal(cty.String), 1363 }), 1364 }), 1365 "single": cty.ObjectVal(map[string]cty.Value{ 1366 "name": cty.NullVal(cty.String), 1367 }), 1368 }), 1369 cty.ObjectVal(map[string]cty.Value{ 1370 "map": cty.MapVal(map[string]cty.Value{ 1371 "one": cty.ObjectVal(map[string]cty.Value{ 1372 "name": cty.NullVal(cty.String), 1373 }), 1374 }), 1375 "map_as_obj": cty.ObjectVal(map[string]cty.Value{ 1376 "one": cty.ObjectVal(map[string]cty.Value{ 1377 "name": cty.StringVal("computed"), 1378 }), 1379 }), 1380 "list": cty.ListVal([]cty.Value{ 1381 cty.ObjectVal(map[string]cty.Value{ 1382 "name": cty.NullVal(cty.String), 1383 }), 1384 }), 1385 "set": cty.SetVal([]cty.Value{ 1386 cty.ObjectVal(map[string]cty.Value{ 1387 "name": cty.NullVal(cty.String), 1388 }), 1389 }), 1390 "single": cty.ObjectVal(map[string]cty.Value{ 1391 "name": cty.NullVal(cty.String), 1392 }), 1393 }), 1394 nil, 1395 }, 1396 "computed nested objects": { 1397 &configschema.Block{ 1398 Attributes: map[string]*configschema.Attribute{ 1399 "map": { 1400 NestedType: &configschema.Object{ 1401 Nesting: configschema.NestingMap, 1402 Attributes: map[string]*configschema.Attribute{ 1403 "name": { 1404 Type: cty.String, 1405 }, 1406 }, 1407 }, 1408 Computed: true, 1409 }, 1410 "list": { 1411 NestedType: &configschema.Object{ 1412 Nesting: configschema.NestingList, 1413 Attributes: map[string]*configschema.Attribute{ 1414 "name": { 1415 Type: cty.String, 1416 }, 1417 }, 1418 }, 1419 Computed: true, 1420 }, 1421 "set": { 1422 NestedType: &configschema.Object{ 1423 Nesting: configschema.NestingSet, 1424 Attributes: map[string]*configschema.Attribute{ 1425 "name": { 1426 Type: cty.String, 1427 }, 1428 }, 1429 }, 1430 Optional: true, 1431 Computed: true, 1432 }, 1433 "single": { 1434 NestedType: &configschema.Object{ 1435 Nesting: configschema.NestingSingle, 1436 Attributes: map[string]*configschema.Attribute{ 1437 "name": { 1438 Type: cty.DynamicPseudoType, 1439 }, 1440 }, 1441 }, 1442 Computed: true, 1443 }, 1444 }, 1445 }, 1446 cty.NullVal(cty.Object(map[string]cty.Type{ 1447 "map": cty.Map(cty.Object(map[string]cty.Type{ 1448 "name": cty.String, 1449 })), 1450 "list": cty.List(cty.Object(map[string]cty.Type{ 1451 "name": cty.String, 1452 })), 1453 "set": cty.Set(cty.Object(map[string]cty.Type{ 1454 "name": cty.String, 1455 })), 1456 "single": cty.Object(map[string]cty.Type{ 1457 "name": cty.String, 1458 }), 1459 })), 1460 cty.ObjectVal(map[string]cty.Value{ 1461 "map": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ 1462 "name": cty.String, 1463 }))), 1464 "list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ 1465 "name": cty.String, 1466 }))), 1467 "set": cty.SetVal([]cty.Value{ 1468 cty.ObjectVal(map[string]cty.Value{ 1469 "name": cty.StringVal("from_config"), 1470 }), 1471 }), 1472 "single": cty.NullVal(cty.Object(map[string]cty.Type{ 1473 "name": cty.String, 1474 })), 1475 }), 1476 cty.ObjectVal(map[string]cty.Value{ 1477 "map": cty.MapVal(map[string]cty.Value{ 1478 "one": cty.UnknownVal(cty.Object(map[string]cty.Type{ 1479 "name": cty.String, 1480 })), 1481 }), 1482 "list": cty.ListVal([]cty.Value{ 1483 cty.ObjectVal(map[string]cty.Value{ 1484 "name": cty.StringVal("computed"), 1485 }), 1486 }), 1487 "set": cty.SetVal([]cty.Value{ 1488 cty.ObjectVal(map[string]cty.Value{ 1489 "name": cty.StringVal("from_config"), 1490 }), 1491 }), 1492 "single": cty.UnknownVal(cty.Object(map[string]cty.Type{ 1493 "name": cty.String, 1494 })), 1495 }), 1496 nil, 1497 }, 1498 "optional computed within nested objects": { 1499 &configschema.Block{ 1500 Attributes: map[string]*configschema.Attribute{ 1501 "map": { 1502 NestedType: &configschema.Object{ 1503 Nesting: configschema.NestingMap, 1504 Attributes: map[string]*configschema.Attribute{ 1505 "name": { 1506 Type: cty.String, 1507 Computed: true, 1508 }, 1509 }, 1510 }, 1511 }, 1512 // When an object has dynamic attrs, the map may be 1513 // handled as an object. 1514 "map_as_obj": { 1515 NestedType: &configschema.Object{ 1516 Nesting: configschema.NestingMap, 1517 Attributes: map[string]*configschema.Attribute{ 1518 "name": { 1519 Type: cty.String, 1520 Optional: true, 1521 Computed: true, 1522 }, 1523 }, 1524 }, 1525 }, 1526 "list": { 1527 NestedType: &configschema.Object{ 1528 Nesting: configschema.NestingList, 1529 Attributes: map[string]*configschema.Attribute{ 1530 "name": { 1531 Type: cty.String, 1532 Optional: true, 1533 Computed: true, 1534 }, 1535 }, 1536 }, 1537 }, 1538 "set": { 1539 NestedType: &configschema.Object{ 1540 Nesting: configschema.NestingSet, 1541 Attributes: map[string]*configschema.Attribute{ 1542 "name": { 1543 Type: cty.String, 1544 Optional: true, 1545 Computed: true, 1546 }, 1547 }, 1548 }, 1549 }, 1550 "single": { 1551 NestedType: &configschema.Object{ 1552 Nesting: configschema.NestingSingle, 1553 Attributes: map[string]*configschema.Attribute{ 1554 "name": { 1555 Type: cty.DynamicPseudoType, 1556 Optional: true, 1557 Computed: true, 1558 }, 1559 }, 1560 }, 1561 }, 1562 }, 1563 }, 1564 cty.NullVal(cty.Object(map[string]cty.Type{ 1565 "map": cty.Map(cty.Object(map[string]cty.Type{ 1566 "name": cty.String, 1567 })), 1568 "map_as_obj": cty.Map(cty.Object(map[string]cty.Type{ 1569 "name": cty.DynamicPseudoType, 1570 })), 1571 "list": cty.List(cty.Object(map[string]cty.Type{ 1572 "name": cty.String, 1573 })), 1574 "set": cty.Set(cty.Object(map[string]cty.Type{ 1575 "name": cty.String, 1576 })), 1577 "single": cty.Object(map[string]cty.Type{ 1578 "name": cty.String, 1579 }), 1580 })), 1581 cty.ObjectVal(map[string]cty.Value{ 1582 "map": cty.MapVal(map[string]cty.Value{ 1583 "one": cty.ObjectVal(map[string]cty.Value{ 1584 "name": cty.StringVal("from_config"), 1585 }), 1586 }), 1587 "map_as_obj": cty.MapVal(map[string]cty.Value{ 1588 "one": cty.ObjectVal(map[string]cty.Value{ 1589 "name": cty.NullVal(cty.DynamicPseudoType), 1590 }), 1591 }), 1592 "list": cty.ListVal([]cty.Value{ 1593 cty.ObjectVal(map[string]cty.Value{ 1594 "name": cty.NullVal(cty.String), 1595 }), 1596 }), 1597 "set": cty.SetVal([]cty.Value{ 1598 cty.ObjectVal(map[string]cty.Value{ 1599 "name": cty.NullVal(cty.String), 1600 }), 1601 }), 1602 "single": cty.ObjectVal(map[string]cty.Value{ 1603 "name": cty.StringVal("from_config"), 1604 }), 1605 }), 1606 cty.ObjectVal(map[string]cty.Value{ 1607 "map": cty.MapVal(map[string]cty.Value{ 1608 "one": cty.ObjectVal(map[string]cty.Value{ 1609 "name": cty.StringVal("from_config"), 1610 }), 1611 }), 1612 "map_as_obj": cty.ObjectVal(map[string]cty.Value{ 1613 "one": cty.ObjectVal(map[string]cty.Value{ 1614 "name": cty.StringVal("computed"), 1615 }), 1616 }), 1617 "list": cty.ListVal([]cty.Value{ 1618 cty.ObjectVal(map[string]cty.Value{ 1619 "name": cty.StringVal("computed"), 1620 }), 1621 }), 1622 "set": cty.SetVal([]cty.Value{ 1623 cty.ObjectVal(map[string]cty.Value{ 1624 "name": cty.NullVal(cty.String), 1625 }), 1626 }), 1627 "single": cty.ObjectVal(map[string]cty.Value{ 1628 "name": cty.StringVal("from_config"), 1629 }), 1630 }), 1631 nil, 1632 }, 1633 "cannot replace config nested attr": { 1634 &configschema.Block{ 1635 Attributes: map[string]*configschema.Attribute{ 1636 "map": { 1637 NestedType: &configschema.Object{ 1638 Nesting: configschema.NestingMap, 1639 Attributes: map[string]*configschema.Attribute{ 1640 "name": { 1641 Type: cty.String, 1642 Computed: true, 1643 Optional: true, 1644 }, 1645 }, 1646 }, 1647 }, 1648 }, 1649 }, 1650 cty.NullVal(cty.Object(map[string]cty.Type{ 1651 "map": cty.Map(cty.Object(map[string]cty.Type{ 1652 "name": cty.String, 1653 })), 1654 })), 1655 cty.ObjectVal(map[string]cty.Value{ 1656 "map": cty.MapVal(map[string]cty.Value{ 1657 "one": cty.ObjectVal(map[string]cty.Value{ 1658 "name": cty.StringVal("from_config"), 1659 }), 1660 }), 1661 }), 1662 cty.ObjectVal(map[string]cty.Value{ 1663 "map": cty.MapVal(map[string]cty.Value{ 1664 "one": cty.ObjectVal(map[string]cty.Value{ 1665 "name": cty.StringVal("from_provider"), 1666 }), 1667 }), 1668 }), 1669 []string{`.map.one.name: planned value cty.StringVal("from_provider") does not match config value cty.StringVal("from_config")`}, 1670 }, 1671 1672 // If a config value ended up in a computed-only attribute it can still 1673 // be a valid plan. We either got here because the user ignore warnings 1674 // about ignore_changes on computed attributes, or we failed to 1675 // validate a config with computed values. Either way, we don't want to 1676 // indicate an error with the provider. 1677 "computed only value with config": { 1678 &configschema.Block{ 1679 Attributes: map[string]*configschema.Attribute{ 1680 "a": { 1681 Type: cty.String, 1682 Computed: true, 1683 }, 1684 }, 1685 }, 1686 cty.ObjectVal(map[string]cty.Value{ 1687 "a": cty.StringVal("old"), 1688 }), 1689 cty.ObjectVal(map[string]cty.Value{ 1690 "a": cty.StringVal("old"), 1691 }), 1692 cty.ObjectVal(map[string]cty.Value{ 1693 "a": cty.UnknownVal(cty.String), 1694 }), 1695 nil, 1696 }, 1697 1698 // When validating collections we start by comparing length, which 1699 // requires guarding for any unknown values incorrectly returned by the 1700 // provider. 1701 "nested collection attrs planned unknown": { 1702 &configschema.Block{ 1703 Attributes: map[string]*configschema.Attribute{ 1704 "set": { 1705 Computed: true, 1706 Optional: true, 1707 NestedType: &configschema.Object{ 1708 Nesting: configschema.NestingSet, 1709 Attributes: map[string]*configschema.Attribute{ 1710 "name": { 1711 Type: cty.String, 1712 Computed: true, 1713 Optional: true, 1714 }, 1715 }, 1716 }, 1717 }, 1718 "list": { 1719 Computed: true, 1720 Optional: true, 1721 NestedType: &configschema.Object{ 1722 Nesting: configschema.NestingList, 1723 Attributes: map[string]*configschema.Attribute{ 1724 "name": { 1725 Type: cty.String, 1726 Computed: true, 1727 Optional: true, 1728 }, 1729 }, 1730 }, 1731 }, 1732 "map": { 1733 Computed: true, 1734 Optional: true, 1735 NestedType: &configschema.Object{ 1736 Nesting: configschema.NestingMap, 1737 Attributes: map[string]*configschema.Attribute{ 1738 "name": { 1739 Type: cty.String, 1740 Computed: true, 1741 Optional: true, 1742 }, 1743 }, 1744 }, 1745 }, 1746 }, 1747 }, 1748 cty.ObjectVal(map[string]cty.Value{ 1749 "set": cty.SetVal([]cty.Value{ 1750 cty.ObjectVal(map[string]cty.Value{ 1751 "name": cty.StringVal("from_config"), 1752 }), 1753 }), 1754 "list": cty.SetVal([]cty.Value{ 1755 cty.ObjectVal(map[string]cty.Value{ 1756 "name": cty.StringVal("from_config"), 1757 }), 1758 }), 1759 "map": cty.MapVal(map[string]cty.Value{ 1760 "key": cty.ObjectVal(map[string]cty.Value{ 1761 "name": cty.StringVal("from_config"), 1762 }), 1763 }), 1764 }), 1765 cty.ObjectVal(map[string]cty.Value{ 1766 "set": cty.SetVal([]cty.Value{ 1767 cty.ObjectVal(map[string]cty.Value{ 1768 "name": cty.StringVal("from_config"), 1769 }), 1770 }), 1771 "list": cty.SetVal([]cty.Value{ 1772 cty.ObjectVal(map[string]cty.Value{ 1773 "name": cty.StringVal("from_config"), 1774 }), 1775 }), 1776 "map": cty.MapVal(map[string]cty.Value{ 1777 "key": cty.ObjectVal(map[string]cty.Value{ 1778 "name": cty.StringVal("from_config"), 1779 }), 1780 }), 1781 }), 1782 // provider cannot override the config 1783 cty.ObjectVal(map[string]cty.Value{ 1784 "set": cty.UnknownVal(cty.Set( 1785 cty.Object(map[string]cty.Type{ 1786 "name": cty.String, 1787 }), 1788 )), 1789 "list": cty.UnknownVal(cty.Set( 1790 cty.Object(map[string]cty.Type{ 1791 "name": cty.String, 1792 }), 1793 )), 1794 "map": cty.UnknownVal(cty.Map( 1795 cty.Object(map[string]cty.Type{ 1796 "name": cty.String, 1797 }), 1798 )), 1799 }), 1800 []string{ 1801 `.set: planned unknown for configured value`, 1802 `.list: planned unknown for configured value`, 1803 `.map: planned unknown for configured value`, 1804 }, 1805 }, 1806 1807 "refined unknown values can become less refined": { 1808 // Providers often can't preserve refinements through the provider 1809 // wire protocol: although we do have a defined serialization for 1810 // it, most providers were written before there was any such 1811 // thing as refinements, and in future there might be new 1812 // refinements that even refinement-aware providers don't know 1813 // how to preserve, so we allow them to get dropped here as 1814 // a concession to backward-compatibility. 1815 &configschema.Block{ 1816 Attributes: map[string]*configschema.Attribute{ 1817 "a": { 1818 Type: cty.String, 1819 Required: true, 1820 }, 1821 }, 1822 }, 1823 cty.ObjectVal(map[string]cty.Value{ 1824 "a": cty.StringVal("old"), 1825 }), 1826 cty.ObjectVal(map[string]cty.Value{ 1827 "a": cty.UnknownVal(cty.String).RefineNotNull(), 1828 }), 1829 cty.ObjectVal(map[string]cty.Value{ 1830 "a": cty.UnknownVal(cty.String), 1831 }), 1832 nil, 1833 }, 1834 1835 "refined unknown values in collection elements can become less refined": { 1836 // Providers often can't preserve refinements through the provider 1837 // wire protocol: although we do have a defined serialization for 1838 // it, most providers were written before there was any such 1839 // thing as refinements, and in future there might be new 1840 // refinements that even refinement-aware providers don't know 1841 // how to preserve, so we allow them to get dropped here as 1842 // a concession to backward-compatibility. 1843 // 1844 // This is intending to approximate something like this: 1845 // 1846 // resource "null_resource" "hello" { 1847 // triggers = { 1848 // key = uuid() 1849 // } 1850 // } 1851 // 1852 // ...under the assumption that the null_resource implementation 1853 // cannot preserve the not-null refinement that the uuid function 1854 // generates. 1855 // 1856 // https://github.com/hashicorp/terraform/issues/33385 1857 &configschema.Block{ 1858 Attributes: map[string]*configschema.Attribute{ 1859 "m": { 1860 Type: cty.Map(cty.String), 1861 }, 1862 }, 1863 }, 1864 cty.NullVal(cty.Object(map[string]cty.Type{ 1865 "m": cty.Map(cty.String), 1866 })), 1867 cty.ObjectVal(map[string]cty.Value{ 1868 "m": cty.MapVal(map[string]cty.Value{ 1869 "key": cty.UnknownVal(cty.String).RefineNotNull(), 1870 }), 1871 }), 1872 cty.ObjectVal(map[string]cty.Value{ 1873 "m": cty.MapVal(map[string]cty.Value{ 1874 "key": cty.UnknownVal(cty.String), 1875 }), 1876 }), 1877 nil, 1878 }, 1879 1880 "nested set values can contain computed unknown": { 1881 &configschema.Block{ 1882 Attributes: map[string]*configschema.Attribute{ 1883 "set": { 1884 Optional: true, 1885 NestedType: &configschema.Object{ 1886 Nesting: configschema.NestingSet, 1887 Attributes: map[string]*configschema.Attribute{ 1888 "input": { 1889 Type: cty.String, 1890 Optional: true, 1891 }, 1892 "computed": { 1893 Type: cty.String, 1894 Computed: true, 1895 Optional: true, 1896 }, 1897 }, 1898 }, 1899 }, 1900 }, 1901 }, 1902 cty.ObjectVal(map[string]cty.Value{ 1903 "set": cty.SetVal([]cty.Value{ 1904 cty.ObjectVal(map[string]cty.Value{ 1905 "input": cty.StringVal("a"), 1906 "computed": cty.NullVal(cty.String), 1907 }), 1908 cty.ObjectVal(map[string]cty.Value{ 1909 "input": cty.StringVal("b"), 1910 "computed": cty.NullVal(cty.String), 1911 }), 1912 }), 1913 }), 1914 cty.ObjectVal(map[string]cty.Value{ 1915 "set": cty.SetVal([]cty.Value{ 1916 cty.ObjectVal(map[string]cty.Value{ 1917 "input": cty.StringVal("a"), 1918 "computed": cty.NullVal(cty.String), 1919 }), 1920 cty.ObjectVal(map[string]cty.Value{ 1921 "input": cty.StringVal("b"), 1922 "computed": cty.NullVal(cty.String), 1923 }), 1924 }), 1925 }), 1926 // Plan can mark the null computed values as unknown 1927 cty.ObjectVal(map[string]cty.Value{ 1928 "set": cty.SetVal([]cty.Value{ 1929 cty.ObjectVal(map[string]cty.Value{ 1930 "input": cty.StringVal("a"), 1931 "computed": cty.UnknownVal(cty.String), 1932 }), 1933 cty.ObjectVal(map[string]cty.Value{ 1934 "input": cty.StringVal("b"), 1935 "computed": cty.UnknownVal(cty.String), 1936 }), 1937 }), 1938 }), 1939 []string{}, 1940 }, 1941 } 1942 1943 for name, test := range tests { 1944 t.Run(name, func(t *testing.T) { 1945 errs := AssertPlanValid(test.Schema, test.Prior, test.Config, test.Planned) 1946 1947 wantErrs := make(map[string]struct{}) 1948 gotErrs := make(map[string]struct{}) 1949 for _, err := range errs { 1950 gotErrs[tfdiags.FormatError(err)] = struct{}{} 1951 } 1952 for _, msg := range test.WantErrs { 1953 wantErrs[msg] = struct{}{} 1954 } 1955 1956 t.Logf( 1957 "\nprior: %sconfig: %splanned: %s", 1958 dump.Value(test.Prior), 1959 dump.Value(test.Config), 1960 dump.Value(test.Planned), 1961 ) 1962 for msg := range wantErrs { 1963 if _, ok := gotErrs[msg]; !ok { 1964 t.Errorf("missing expected error: %s", msg) 1965 } 1966 } 1967 for msg := range gotErrs { 1968 if _, ok := wantErrs[msg]; !ok { 1969 t.Errorf("unexpected extra error: %s", msg) 1970 } 1971 } 1972 }) 1973 } 1974 }