github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/command/format/diff_test.go (about) 1 package format 2 3 import ( 4 "testing" 5 6 "github.com/hashicorp/terraform-plugin-sdk/internal/addrs" 7 "github.com/hashicorp/terraform-plugin-sdk/internal/configs/configschema" 8 "github.com/hashicorp/terraform-plugin-sdk/internal/plans" 9 "github.com/mitchellh/colorstring" 10 "github.com/zclconf/go-cty/cty" 11 ) 12 13 func TestResourceChange_primitiveTypes(t *testing.T) { 14 testCases := map[string]testCase{ 15 "creation": { 16 Action: plans.Create, 17 Mode: addrs.ManagedResourceMode, 18 Before: cty.NullVal(cty.EmptyObject), 19 After: cty.ObjectVal(map[string]cty.Value{ 20 "id": cty.UnknownVal(cty.String), 21 }), 22 Schema: &configschema.Block{ 23 Attributes: map[string]*configschema.Attribute{ 24 "id": {Type: cty.String, Computed: true}, 25 }, 26 }, 27 RequiredReplace: cty.NewPathSet(), 28 Tainted: false, 29 ExpectedOutput: ` # test_instance.example will be created 30 + resource "test_instance" "example" { 31 + id = (known after apply) 32 } 33 `, 34 }, 35 "creation (null string)": { 36 Action: plans.Create, 37 Mode: addrs.ManagedResourceMode, 38 Before: cty.NullVal(cty.EmptyObject), 39 After: cty.ObjectVal(map[string]cty.Value{ 40 "string": cty.StringVal("null"), 41 }), 42 Schema: &configschema.Block{ 43 Attributes: map[string]*configschema.Attribute{ 44 "string": {Type: cty.String, Optional: true}, 45 }, 46 }, 47 RequiredReplace: cty.NewPathSet(), 48 Tainted: false, 49 ExpectedOutput: ` # test_instance.example will be created 50 + resource "test_instance" "example" { 51 + string = "null" 52 } 53 `, 54 }, 55 "deletion": { 56 Action: plans.Delete, 57 Mode: addrs.ManagedResourceMode, 58 Before: cty.ObjectVal(map[string]cty.Value{ 59 "id": cty.StringVal("i-02ae66f368e8518a9"), 60 }), 61 After: cty.NullVal(cty.EmptyObject), 62 Schema: &configschema.Block{ 63 Attributes: map[string]*configschema.Attribute{ 64 "id": {Type: cty.String, Computed: true}, 65 }, 66 }, 67 RequiredReplace: cty.NewPathSet(), 68 Tainted: false, 69 ExpectedOutput: ` # test_instance.example will be destroyed 70 - resource "test_instance" "example" { 71 - id = "i-02ae66f368e8518a9" -> null 72 } 73 `, 74 }, 75 "deletion (empty string)": { 76 Action: plans.Delete, 77 Mode: addrs.ManagedResourceMode, 78 Before: cty.ObjectVal(map[string]cty.Value{ 79 "id": cty.StringVal("i-02ae66f368e8518a9"), 80 "intentionally_long": cty.StringVal(""), 81 }), 82 After: cty.NullVal(cty.EmptyObject), 83 Schema: &configschema.Block{ 84 Attributes: map[string]*configschema.Attribute{ 85 "id": {Type: cty.String, Computed: true}, 86 "intentionally_long": {Type: cty.String, Optional: true}, 87 }, 88 }, 89 RequiredReplace: cty.NewPathSet(), 90 Tainted: false, 91 ExpectedOutput: ` # test_instance.example will be destroyed 92 - resource "test_instance" "example" { 93 - id = "i-02ae66f368e8518a9" -> null 94 } 95 `, 96 }, 97 "string in-place update": { 98 Action: plans.Update, 99 Mode: addrs.ManagedResourceMode, 100 Before: cty.ObjectVal(map[string]cty.Value{ 101 "id": cty.StringVal("i-02ae66f368e8518a9"), 102 "ami": cty.StringVal("ami-BEFORE"), 103 }), 104 After: cty.ObjectVal(map[string]cty.Value{ 105 "id": cty.StringVal("i-02ae66f368e8518a9"), 106 "ami": cty.StringVal("ami-AFTER"), 107 }), 108 Schema: &configschema.Block{ 109 Attributes: map[string]*configschema.Attribute{ 110 "id": {Type: cty.String, Optional: true, Computed: true}, 111 "ami": {Type: cty.String, Optional: true}, 112 }, 113 }, 114 RequiredReplace: cty.NewPathSet(), 115 Tainted: false, 116 ExpectedOutput: ` # test_instance.example will be updated in-place 117 ~ resource "test_instance" "example" { 118 ~ ami = "ami-BEFORE" -> "ami-AFTER" 119 id = "i-02ae66f368e8518a9" 120 } 121 `, 122 }, 123 "string force-new update": { 124 Action: plans.DeleteThenCreate, 125 Mode: addrs.ManagedResourceMode, 126 Before: cty.ObjectVal(map[string]cty.Value{ 127 "id": cty.StringVal("i-02ae66f368e8518a9"), 128 "ami": cty.StringVal("ami-BEFORE"), 129 }), 130 After: cty.ObjectVal(map[string]cty.Value{ 131 "id": cty.StringVal("i-02ae66f368e8518a9"), 132 "ami": cty.StringVal("ami-AFTER"), 133 }), 134 Schema: &configschema.Block{ 135 Attributes: map[string]*configschema.Attribute{ 136 "id": {Type: cty.String, Optional: true, Computed: true}, 137 "ami": {Type: cty.String, Optional: true}, 138 }, 139 }, 140 RequiredReplace: cty.NewPathSet(cty.Path{ 141 cty.GetAttrStep{Name: "ami"}, 142 }), 143 Tainted: false, 144 ExpectedOutput: ` # test_instance.example must be replaced 145 -/+ resource "test_instance" "example" { 146 ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement 147 id = "i-02ae66f368e8518a9" 148 } 149 `, 150 }, 151 "string in-place update (null values)": { 152 Action: plans.Update, 153 Mode: addrs.ManagedResourceMode, 154 Before: cty.ObjectVal(map[string]cty.Value{ 155 "id": cty.StringVal("i-02ae66f368e8518a9"), 156 "ami": cty.StringVal("ami-BEFORE"), 157 "unchanged": cty.NullVal(cty.String), 158 }), 159 After: cty.ObjectVal(map[string]cty.Value{ 160 "id": cty.StringVal("i-02ae66f368e8518a9"), 161 "ami": cty.StringVal("ami-AFTER"), 162 "unchanged": cty.NullVal(cty.String), 163 }), 164 Schema: &configschema.Block{ 165 Attributes: map[string]*configschema.Attribute{ 166 "id": {Type: cty.String, Optional: true, Computed: true}, 167 "ami": {Type: cty.String, Optional: true}, 168 "unchanged": {Type: cty.String, Optional: true}, 169 }, 170 }, 171 RequiredReplace: cty.NewPathSet(), 172 Tainted: false, 173 ExpectedOutput: ` # test_instance.example will be updated in-place 174 ~ resource "test_instance" "example" { 175 ~ ami = "ami-BEFORE" -> "ami-AFTER" 176 id = "i-02ae66f368e8518a9" 177 } 178 `, 179 }, 180 "in-place update of multi-line string field": { 181 Action: plans.Update, 182 Mode: addrs.ManagedResourceMode, 183 Before: cty.ObjectVal(map[string]cty.Value{ 184 "id": cty.StringVal("i-02ae66f368e8518a9"), 185 "more_lines": cty.StringVal(`original 186 `), 187 }), 188 After: cty.ObjectVal(map[string]cty.Value{ 189 "id": cty.UnknownVal(cty.String), 190 "more_lines": cty.StringVal(`original 191 new line 192 `), 193 }), 194 Schema: &configschema.Block{ 195 Attributes: map[string]*configschema.Attribute{ 196 "id": {Type: cty.String, Optional: true, Computed: true}, 197 "more_lines": {Type: cty.String, Optional: true}, 198 }, 199 }, 200 RequiredReplace: cty.NewPathSet(), 201 Tainted: false, 202 ExpectedOutput: ` # test_instance.example will be updated in-place 203 ~ resource "test_instance" "example" { 204 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 205 ~ more_lines = <<~EOT 206 original 207 + new line 208 EOT 209 } 210 `, 211 }, 212 "force-new update of multi-line string field": { 213 Action: plans.DeleteThenCreate, 214 Mode: addrs.ManagedResourceMode, 215 Before: cty.ObjectVal(map[string]cty.Value{ 216 "id": cty.StringVal("i-02ae66f368e8518a9"), 217 "more_lines": cty.StringVal(`original 218 `), 219 }), 220 After: cty.ObjectVal(map[string]cty.Value{ 221 "id": cty.UnknownVal(cty.String), 222 "more_lines": cty.StringVal(`original 223 new line 224 `), 225 }), 226 Schema: &configschema.Block{ 227 Attributes: map[string]*configschema.Attribute{ 228 "id": {Type: cty.String, Optional: true, Computed: true}, 229 "more_lines": {Type: cty.String, Optional: true}, 230 }, 231 }, 232 RequiredReplace: cty.NewPathSet(cty.Path{ 233 cty.GetAttrStep{Name: "more_lines"}, 234 }), 235 Tainted: false, 236 ExpectedOutput: ` # test_instance.example must be replaced 237 -/+ resource "test_instance" "example" { 238 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 239 ~ more_lines = <<~EOT # forces replacement 240 original 241 + new line 242 EOT 243 } 244 `, 245 }, 246 247 // Sensitive 248 249 "creation with sensitive field": { 250 Action: plans.Create, 251 Mode: addrs.ManagedResourceMode, 252 Before: cty.NullVal(cty.EmptyObject), 253 After: cty.ObjectVal(map[string]cty.Value{ 254 "id": cty.UnknownVal(cty.String), 255 "password": cty.StringVal("top-secret"), 256 }), 257 Schema: &configschema.Block{ 258 Attributes: map[string]*configschema.Attribute{ 259 "id": {Type: cty.String, Computed: true}, 260 "password": {Type: cty.String, Optional: true, Sensitive: true}, 261 }, 262 }, 263 RequiredReplace: cty.NewPathSet(), 264 Tainted: false, 265 ExpectedOutput: ` # test_instance.example will be created 266 + resource "test_instance" "example" { 267 + id = (known after apply) 268 + password = (sensitive value) 269 } 270 `, 271 }, 272 "update with equal sensitive field": { 273 Action: plans.Update, 274 Mode: addrs.ManagedResourceMode, 275 Before: cty.ObjectVal(map[string]cty.Value{ 276 "id": cty.StringVal("blah"), 277 "str": cty.StringVal("before"), 278 "password": cty.StringVal("top-secret"), 279 }), 280 After: cty.ObjectVal(map[string]cty.Value{ 281 "id": cty.UnknownVal(cty.String), 282 "str": cty.StringVal("after"), 283 "password": cty.StringVal("top-secret"), 284 }), 285 Schema: &configschema.Block{ 286 Attributes: map[string]*configschema.Attribute{ 287 "id": {Type: cty.String, Computed: true}, 288 "str": {Type: cty.String, Optional: true}, 289 "password": {Type: cty.String, Optional: true, Sensitive: true}, 290 }, 291 }, 292 RequiredReplace: cty.NewPathSet(), 293 Tainted: false, 294 ExpectedOutput: ` # test_instance.example will be updated in-place 295 ~ resource "test_instance" "example" { 296 ~ id = "blah" -> (known after apply) 297 password = (sensitive value) 298 ~ str = "before" -> "after" 299 } 300 `, 301 }, 302 303 // tainted resources 304 "replace tainted resource": { 305 Action: plans.DeleteThenCreate, 306 Mode: addrs.ManagedResourceMode, 307 Before: cty.ObjectVal(map[string]cty.Value{ 308 "id": cty.StringVal("i-02ae66f368e8518a9"), 309 "ami": cty.StringVal("ami-BEFORE"), 310 }), 311 After: cty.ObjectVal(map[string]cty.Value{ 312 "id": cty.UnknownVal(cty.String), 313 "ami": cty.StringVal("ami-AFTER"), 314 }), 315 Schema: &configschema.Block{ 316 Attributes: map[string]*configschema.Attribute{ 317 "id": {Type: cty.String, Optional: true, Computed: true}, 318 "ami": {Type: cty.String, Optional: true}, 319 }, 320 }, 321 RequiredReplace: cty.NewPathSet(cty.Path{ 322 cty.GetAttrStep{Name: "ami"}, 323 }), 324 Tainted: true, 325 ExpectedOutput: ` # test_instance.example is tainted, so must be replaced 326 -/+ resource "test_instance" "example" { 327 ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement 328 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 329 } 330 `, 331 }, 332 "force replacement with empty before value": { 333 Action: plans.DeleteThenCreate, 334 Mode: addrs.ManagedResourceMode, 335 Before: cty.ObjectVal(map[string]cty.Value{ 336 "name": cty.StringVal("name"), 337 "forced": cty.NullVal(cty.String), 338 }), 339 After: cty.ObjectVal(map[string]cty.Value{ 340 "name": cty.StringVal("name"), 341 "forced": cty.StringVal("example"), 342 }), 343 Schema: &configschema.Block{ 344 Attributes: map[string]*configschema.Attribute{ 345 "name": {Type: cty.String, Optional: true}, 346 "forced": {Type: cty.String, Optional: true}, 347 }, 348 }, 349 RequiredReplace: cty.NewPathSet(cty.Path{ 350 cty.GetAttrStep{Name: "forced"}, 351 }), 352 Tainted: false, 353 ExpectedOutput: ` # test_instance.example must be replaced 354 -/+ resource "test_instance" "example" { 355 + forced = "example" # forces replacement 356 name = "name" 357 } 358 `, 359 }, 360 "force replacement with empty before value legacy": { 361 Action: plans.DeleteThenCreate, 362 Mode: addrs.ManagedResourceMode, 363 Before: cty.ObjectVal(map[string]cty.Value{ 364 "name": cty.StringVal("name"), 365 "forced": cty.StringVal(""), 366 }), 367 After: cty.ObjectVal(map[string]cty.Value{ 368 "name": cty.StringVal("name"), 369 "forced": cty.StringVal("example"), 370 }), 371 Schema: &configschema.Block{ 372 Attributes: map[string]*configschema.Attribute{ 373 "name": {Type: cty.String, Optional: true}, 374 "forced": {Type: cty.String, Optional: true}, 375 }, 376 }, 377 RequiredReplace: cty.NewPathSet(cty.Path{ 378 cty.GetAttrStep{Name: "forced"}, 379 }), 380 Tainted: false, 381 ExpectedOutput: ` # test_instance.example must be replaced 382 -/+ resource "test_instance" "example" { 383 + forced = "example" # forces replacement 384 name = "name" 385 } 386 `, 387 }, 388 } 389 390 runTestCases(t, testCases) 391 } 392 393 func TestResourceChange_JSON(t *testing.T) { 394 testCases := map[string]testCase{ 395 "creation": { 396 Action: plans.Create, 397 Mode: addrs.ManagedResourceMode, 398 Before: cty.NullVal(cty.EmptyObject), 399 After: cty.ObjectVal(map[string]cty.Value{ 400 "id": cty.UnknownVal(cty.String), 401 "json_field": cty.StringVal(`{ 402 "str": "value", 403 "list":["a","b", 234, true], 404 "obj": {"key": "val"} 405 }`), 406 }), 407 Schema: &configschema.Block{ 408 Attributes: map[string]*configschema.Attribute{ 409 "id": {Type: cty.String, Optional: true, Computed: true}, 410 "json_field": {Type: cty.String, Optional: true}, 411 }, 412 }, 413 RequiredReplace: cty.NewPathSet(), 414 Tainted: false, 415 ExpectedOutput: ` # test_instance.example will be created 416 + resource "test_instance" "example" { 417 + id = (known after apply) 418 + json_field = jsonencode( 419 { 420 + list = [ 421 + "a", 422 + "b", 423 + 234, 424 + true, 425 ] 426 + obj = { 427 + key = "val" 428 } 429 + str = "value" 430 } 431 ) 432 } 433 `, 434 }, 435 "in-place update of object": { 436 Action: plans.Update, 437 Mode: addrs.ManagedResourceMode, 438 Before: cty.ObjectVal(map[string]cty.Value{ 439 "id": cty.StringVal("i-02ae66f368e8518a9"), 440 "json_field": cty.StringVal(`{"aaa": "value"}`), 441 }), 442 After: cty.ObjectVal(map[string]cty.Value{ 443 "id": cty.UnknownVal(cty.String), 444 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`), 445 }), 446 Schema: &configschema.Block{ 447 Attributes: map[string]*configschema.Attribute{ 448 "id": {Type: cty.String, Optional: true, Computed: true}, 449 "json_field": {Type: cty.String, Optional: true}, 450 }, 451 }, 452 RequiredReplace: cty.NewPathSet(), 453 Tainted: false, 454 ExpectedOutput: ` # test_instance.example will be updated in-place 455 ~ resource "test_instance" "example" { 456 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 457 ~ json_field = jsonencode( 458 ~ { 459 aaa = "value" 460 + bbb = "new_value" 461 } 462 ) 463 } 464 `, 465 }, 466 "in-place update (from empty tuple)": { 467 Action: plans.Update, 468 Mode: addrs.ManagedResourceMode, 469 Before: cty.ObjectVal(map[string]cty.Value{ 470 "id": cty.StringVal("i-02ae66f368e8518a9"), 471 "json_field": cty.StringVal(`{"aaa": []}`), 472 }), 473 After: cty.ObjectVal(map[string]cty.Value{ 474 "id": cty.UnknownVal(cty.String), 475 "json_field": cty.StringVal(`{"aaa": ["value"]}`), 476 }), 477 Schema: &configschema.Block{ 478 Attributes: map[string]*configschema.Attribute{ 479 "id": {Type: cty.String, Optional: true, Computed: true}, 480 "json_field": {Type: cty.String, Optional: true}, 481 }, 482 }, 483 RequiredReplace: cty.NewPathSet(), 484 Tainted: false, 485 ExpectedOutput: ` # test_instance.example will be updated in-place 486 ~ resource "test_instance" "example" { 487 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 488 ~ json_field = jsonencode( 489 ~ { 490 ~ aaa = [ 491 + "value", 492 ] 493 } 494 ) 495 } 496 `, 497 }, 498 "in-place update (to empty tuple)": { 499 Action: plans.Update, 500 Mode: addrs.ManagedResourceMode, 501 Before: cty.ObjectVal(map[string]cty.Value{ 502 "id": cty.StringVal("i-02ae66f368e8518a9"), 503 "json_field": cty.StringVal(`{"aaa": ["value"]}`), 504 }), 505 After: cty.ObjectVal(map[string]cty.Value{ 506 "id": cty.UnknownVal(cty.String), 507 "json_field": cty.StringVal(`{"aaa": []}`), 508 }), 509 Schema: &configschema.Block{ 510 Attributes: map[string]*configschema.Attribute{ 511 "id": {Type: cty.String, Optional: true, Computed: true}, 512 "json_field": {Type: cty.String, Optional: true}, 513 }, 514 }, 515 RequiredReplace: cty.NewPathSet(), 516 Tainted: false, 517 ExpectedOutput: ` # test_instance.example will be updated in-place 518 ~ resource "test_instance" "example" { 519 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 520 ~ json_field = jsonencode( 521 ~ { 522 ~ aaa = [ 523 - "value", 524 ] 525 } 526 ) 527 } 528 `, 529 }, 530 "in-place update (tuple of different types)": { 531 Action: plans.Update, 532 Mode: addrs.ManagedResourceMode, 533 Before: cty.ObjectVal(map[string]cty.Value{ 534 "id": cty.StringVal("i-02ae66f368e8518a9"), 535 "json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`), 536 }), 537 After: cty.ObjectVal(map[string]cty.Value{ 538 "id": cty.UnknownVal(cty.String), 539 "json_field": cty.StringVal(`{"aaa": [42, {"foo":"baz"}, "value"]}`), 540 }), 541 Schema: &configschema.Block{ 542 Attributes: map[string]*configschema.Attribute{ 543 "id": {Type: cty.String, Optional: true, Computed: true}, 544 "json_field": {Type: cty.String, Optional: true}, 545 }, 546 }, 547 RequiredReplace: cty.NewPathSet(), 548 Tainted: false, 549 ExpectedOutput: ` # test_instance.example will be updated in-place 550 ~ resource "test_instance" "example" { 551 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 552 ~ json_field = jsonencode( 553 ~ { 554 ~ aaa = [ 555 42, 556 ~ { 557 ~ foo = "bar" -> "baz" 558 }, 559 "value", 560 ] 561 } 562 ) 563 } 564 `, 565 }, 566 "force-new update": { 567 Action: plans.DeleteThenCreate, 568 Mode: addrs.ManagedResourceMode, 569 Before: cty.ObjectVal(map[string]cty.Value{ 570 "id": cty.StringVal("i-02ae66f368e8518a9"), 571 "json_field": cty.StringVal(`{"aaa": "value"}`), 572 }), 573 After: cty.ObjectVal(map[string]cty.Value{ 574 "id": cty.UnknownVal(cty.String), 575 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`), 576 }), 577 Schema: &configschema.Block{ 578 Attributes: map[string]*configschema.Attribute{ 579 "id": {Type: cty.String, Optional: true, Computed: true}, 580 "json_field": {Type: cty.String, Optional: true}, 581 }, 582 }, 583 RequiredReplace: cty.NewPathSet(cty.Path{ 584 cty.GetAttrStep{Name: "json_field"}, 585 }), 586 Tainted: false, 587 ExpectedOutput: ` # test_instance.example must be replaced 588 -/+ resource "test_instance" "example" { 589 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 590 ~ json_field = jsonencode( 591 ~ { 592 aaa = "value" 593 + bbb = "new_value" 594 } # forces replacement 595 ) 596 } 597 `, 598 }, 599 "in-place update (whitespace change)": { 600 Action: plans.Update, 601 Mode: addrs.ManagedResourceMode, 602 Before: cty.ObjectVal(map[string]cty.Value{ 603 "id": cty.StringVal("i-02ae66f368e8518a9"), 604 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`), 605 }), 606 After: cty.ObjectVal(map[string]cty.Value{ 607 "id": cty.UnknownVal(cty.String), 608 "json_field": cty.StringVal(`{"aaa":"value", 609 "bbb":"another"}`), 610 }), 611 Schema: &configschema.Block{ 612 Attributes: map[string]*configschema.Attribute{ 613 "id": {Type: cty.String, Optional: true, Computed: true}, 614 "json_field": {Type: cty.String, Optional: true}, 615 }, 616 }, 617 RequiredReplace: cty.NewPathSet(), 618 Tainted: false, 619 ExpectedOutput: ` # test_instance.example will be updated in-place 620 ~ resource "test_instance" "example" { 621 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 622 ~ json_field = jsonencode( # whitespace changes 623 { 624 aaa = "value" 625 bbb = "another" 626 } 627 ) 628 } 629 `, 630 }, 631 "force-new update (whitespace change)": { 632 Action: plans.DeleteThenCreate, 633 Mode: addrs.ManagedResourceMode, 634 Before: cty.ObjectVal(map[string]cty.Value{ 635 "id": cty.StringVal("i-02ae66f368e8518a9"), 636 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`), 637 }), 638 After: cty.ObjectVal(map[string]cty.Value{ 639 "id": cty.UnknownVal(cty.String), 640 "json_field": cty.StringVal(`{"aaa":"value", 641 "bbb":"another"}`), 642 }), 643 Schema: &configschema.Block{ 644 Attributes: map[string]*configschema.Attribute{ 645 "id": {Type: cty.String, Optional: true, Computed: true}, 646 "json_field": {Type: cty.String, Optional: true}, 647 }, 648 }, 649 RequiredReplace: cty.NewPathSet(cty.Path{ 650 cty.GetAttrStep{Name: "json_field"}, 651 }), 652 Tainted: false, 653 ExpectedOutput: ` # test_instance.example must be replaced 654 -/+ resource "test_instance" "example" { 655 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 656 ~ json_field = jsonencode( # whitespace changes force replacement 657 { 658 aaa = "value" 659 bbb = "another" 660 } 661 ) 662 } 663 `, 664 }, 665 "creation (empty)": { 666 Action: plans.Create, 667 Mode: addrs.ManagedResourceMode, 668 Before: cty.NullVal(cty.EmptyObject), 669 After: cty.ObjectVal(map[string]cty.Value{ 670 "id": cty.UnknownVal(cty.String), 671 "json_field": cty.StringVal(`{}`), 672 }), 673 Schema: &configschema.Block{ 674 Attributes: map[string]*configschema.Attribute{ 675 "id": {Type: cty.String, Optional: true, Computed: true}, 676 "json_field": {Type: cty.String, Optional: true}, 677 }, 678 }, 679 RequiredReplace: cty.NewPathSet(), 680 Tainted: false, 681 ExpectedOutput: ` # test_instance.example will be created 682 + resource "test_instance" "example" { 683 + id = (known after apply) 684 + json_field = jsonencode({}) 685 } 686 `, 687 }, 688 "JSON list item removal": { 689 Action: plans.Update, 690 Mode: addrs.ManagedResourceMode, 691 Before: cty.ObjectVal(map[string]cty.Value{ 692 "id": cty.StringVal("i-02ae66f368e8518a9"), 693 "json_field": cty.StringVal(`["first","second","third"]`), 694 }), 695 After: cty.ObjectVal(map[string]cty.Value{ 696 "id": cty.UnknownVal(cty.String), 697 "json_field": cty.StringVal(`["first","second"]`), 698 }), 699 Schema: &configschema.Block{ 700 Attributes: map[string]*configschema.Attribute{ 701 "id": {Type: cty.String, Optional: true, Computed: true}, 702 "json_field": {Type: cty.String, Optional: true}, 703 }, 704 }, 705 RequiredReplace: cty.NewPathSet(), 706 Tainted: false, 707 ExpectedOutput: ` # test_instance.example will be updated in-place 708 ~ resource "test_instance" "example" { 709 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 710 ~ json_field = jsonencode( 711 ~ [ 712 "first", 713 "second", 714 - "third", 715 ] 716 ) 717 } 718 `, 719 }, 720 "JSON list item addition": { 721 Action: plans.Update, 722 Mode: addrs.ManagedResourceMode, 723 Before: cty.ObjectVal(map[string]cty.Value{ 724 "id": cty.StringVal("i-02ae66f368e8518a9"), 725 "json_field": cty.StringVal(`["first","second"]`), 726 }), 727 After: cty.ObjectVal(map[string]cty.Value{ 728 "id": cty.UnknownVal(cty.String), 729 "json_field": cty.StringVal(`["first","second","third"]`), 730 }), 731 Schema: &configschema.Block{ 732 Attributes: map[string]*configschema.Attribute{ 733 "id": {Type: cty.String, Optional: true, Computed: true}, 734 "json_field": {Type: cty.String, Optional: true}, 735 }, 736 }, 737 RequiredReplace: cty.NewPathSet(), 738 Tainted: false, 739 ExpectedOutput: ` # test_instance.example will be updated in-place 740 ~ resource "test_instance" "example" { 741 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 742 ~ json_field = jsonencode( 743 ~ [ 744 "first", 745 "second", 746 + "third", 747 ] 748 ) 749 } 750 `, 751 }, 752 "JSON list object addition": { 753 Action: plans.Update, 754 Mode: addrs.ManagedResourceMode, 755 Before: cty.ObjectVal(map[string]cty.Value{ 756 "id": cty.StringVal("i-02ae66f368e8518a9"), 757 "json_field": cty.StringVal(`{"first":"111"}`), 758 }), 759 After: cty.ObjectVal(map[string]cty.Value{ 760 "id": cty.UnknownVal(cty.String), 761 "json_field": cty.StringVal(`{"first":"111","second":"222"}`), 762 }), 763 Schema: &configschema.Block{ 764 Attributes: map[string]*configschema.Attribute{ 765 "id": {Type: cty.String, Optional: true, Computed: true}, 766 "json_field": {Type: cty.String, Optional: true}, 767 }, 768 }, 769 RequiredReplace: cty.NewPathSet(), 770 Tainted: false, 771 ExpectedOutput: ` # test_instance.example will be updated in-place 772 ~ resource "test_instance" "example" { 773 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 774 ~ json_field = jsonencode( 775 ~ { 776 first = "111" 777 + second = "222" 778 } 779 ) 780 } 781 `, 782 }, 783 "JSON object with nested list": { 784 Action: plans.Update, 785 Mode: addrs.ManagedResourceMode, 786 Before: cty.ObjectVal(map[string]cty.Value{ 787 "id": cty.StringVal("i-02ae66f368e8518a9"), 788 "json_field": cty.StringVal(`{ 789 "Statement": ["first"] 790 }`), 791 }), 792 After: cty.ObjectVal(map[string]cty.Value{ 793 "id": cty.UnknownVal(cty.String), 794 "json_field": cty.StringVal(`{ 795 "Statement": ["first", "second"] 796 }`), 797 }), 798 Schema: &configschema.Block{ 799 Attributes: map[string]*configschema.Attribute{ 800 "id": {Type: cty.String, Optional: true, Computed: true}, 801 "json_field": {Type: cty.String, Optional: true}, 802 }, 803 }, 804 RequiredReplace: cty.NewPathSet(), 805 Tainted: false, 806 ExpectedOutput: ` # test_instance.example will be updated in-place 807 ~ resource "test_instance" "example" { 808 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 809 ~ json_field = jsonencode( 810 ~ { 811 ~ Statement = [ 812 "first", 813 + "second", 814 ] 815 } 816 ) 817 } 818 `, 819 }, 820 "JSON list of objects - adding item": { 821 Action: plans.Update, 822 Mode: addrs.ManagedResourceMode, 823 Before: cty.ObjectVal(map[string]cty.Value{ 824 "id": cty.StringVal("i-02ae66f368e8518a9"), 825 "json_field": cty.StringVal(`[{"one": "111"}]`), 826 }), 827 After: cty.ObjectVal(map[string]cty.Value{ 828 "id": cty.UnknownVal(cty.String), 829 "json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`), 830 }), 831 Schema: &configschema.Block{ 832 Attributes: map[string]*configschema.Attribute{ 833 "id": {Type: cty.String, Optional: true, Computed: true}, 834 "json_field": {Type: cty.String, Optional: true}, 835 }, 836 }, 837 RequiredReplace: cty.NewPathSet(), 838 Tainted: false, 839 ExpectedOutput: ` # test_instance.example will be updated in-place 840 ~ resource "test_instance" "example" { 841 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 842 ~ json_field = jsonencode( 843 ~ [ 844 { 845 one = "111" 846 }, 847 + { 848 + two = "222" 849 }, 850 ] 851 ) 852 } 853 `, 854 }, 855 "JSON list of objects - removing item": { 856 Action: plans.Update, 857 Mode: addrs.ManagedResourceMode, 858 Before: cty.ObjectVal(map[string]cty.Value{ 859 "id": cty.StringVal("i-02ae66f368e8518a9"), 860 "json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`), 861 }), 862 After: cty.ObjectVal(map[string]cty.Value{ 863 "id": cty.UnknownVal(cty.String), 864 "json_field": cty.StringVal(`[{"one": "111"}]`), 865 }), 866 Schema: &configschema.Block{ 867 Attributes: map[string]*configschema.Attribute{ 868 "id": {Type: cty.String, Optional: true, Computed: true}, 869 "json_field": {Type: cty.String, Optional: true}, 870 }, 871 }, 872 RequiredReplace: cty.NewPathSet(), 873 Tainted: false, 874 ExpectedOutput: ` # test_instance.example will be updated in-place 875 ~ resource "test_instance" "example" { 876 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 877 ~ json_field = jsonencode( 878 ~ [ 879 { 880 one = "111" 881 }, 882 - { 883 - two = "222" 884 }, 885 ] 886 ) 887 } 888 `, 889 }, 890 "JSON object with list of objects": { 891 Action: plans.Update, 892 Mode: addrs.ManagedResourceMode, 893 Before: cty.ObjectVal(map[string]cty.Value{ 894 "id": cty.StringVal("i-02ae66f368e8518a9"), 895 "json_field": cty.StringVal(`{"parent":[{"one": "111"}]}`), 896 }), 897 After: cty.ObjectVal(map[string]cty.Value{ 898 "id": cty.UnknownVal(cty.String), 899 "json_field": cty.StringVal(`{"parent":[{"one": "111"}, {"two": "222"}]}`), 900 }), 901 Schema: &configschema.Block{ 902 Attributes: map[string]*configschema.Attribute{ 903 "id": {Type: cty.String, Optional: true, Computed: true}, 904 "json_field": {Type: cty.String, Optional: true}, 905 }, 906 }, 907 RequiredReplace: cty.NewPathSet(), 908 Tainted: false, 909 ExpectedOutput: ` # test_instance.example will be updated in-place 910 ~ resource "test_instance" "example" { 911 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 912 ~ json_field = jsonencode( 913 ~ { 914 ~ parent = [ 915 { 916 one = "111" 917 }, 918 + { 919 + two = "222" 920 }, 921 ] 922 } 923 ) 924 } 925 `, 926 }, 927 "JSON object double nested lists": { 928 Action: plans.Update, 929 Mode: addrs.ManagedResourceMode, 930 Before: cty.ObjectVal(map[string]cty.Value{ 931 "id": cty.StringVal("i-02ae66f368e8518a9"), 932 "json_field": cty.StringVal(`{"parent":[{"another_list": ["111"]}]}`), 933 }), 934 After: cty.ObjectVal(map[string]cty.Value{ 935 "id": cty.UnknownVal(cty.String), 936 "json_field": cty.StringVal(`{"parent":[{"another_list": ["111", "222"]}]}`), 937 }), 938 Schema: &configschema.Block{ 939 Attributes: map[string]*configschema.Attribute{ 940 "id": {Type: cty.String, Optional: true, Computed: true}, 941 "json_field": {Type: cty.String, Optional: true}, 942 }, 943 }, 944 RequiredReplace: cty.NewPathSet(), 945 Tainted: false, 946 ExpectedOutput: ` # test_instance.example will be updated in-place 947 ~ resource "test_instance" "example" { 948 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 949 ~ json_field = jsonencode( 950 ~ { 951 ~ parent = [ 952 ~ { 953 ~ another_list = [ 954 "111", 955 + "222", 956 ] 957 }, 958 ] 959 } 960 ) 961 } 962 `, 963 }, 964 "in-place update from object to tuple": { 965 Action: plans.Update, 966 Mode: addrs.ManagedResourceMode, 967 Before: cty.ObjectVal(map[string]cty.Value{ 968 "id": cty.StringVal("i-02ae66f368e8518a9"), 969 "json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`), 970 }), 971 After: cty.ObjectVal(map[string]cty.Value{ 972 "id": cty.UnknownVal(cty.String), 973 "json_field": cty.StringVal(`["aaa", 42, "something"]`), 974 }), 975 Schema: &configschema.Block{ 976 Attributes: map[string]*configschema.Attribute{ 977 "id": {Type: cty.String, Optional: true, Computed: true}, 978 "json_field": {Type: cty.String, Optional: true}, 979 }, 980 }, 981 RequiredReplace: cty.NewPathSet(), 982 Tainted: false, 983 ExpectedOutput: ` # test_instance.example will be updated in-place 984 ~ resource "test_instance" "example" { 985 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 986 ~ json_field = jsonencode( 987 ~ { 988 - aaa = [ 989 - 42, 990 - { 991 - foo = "bar" 992 }, 993 - "value", 994 ] 995 } -> [ 996 + "aaa", 997 + 42, 998 + "something", 999 ] 1000 ) 1001 } 1002 `, 1003 }, 1004 } 1005 runTestCases(t, testCases) 1006 } 1007 1008 func TestResourceChange_primitiveList(t *testing.T) { 1009 testCases := map[string]testCase{ 1010 "in-place update - creation": { 1011 Action: plans.Update, 1012 Mode: addrs.ManagedResourceMode, 1013 Before: cty.ObjectVal(map[string]cty.Value{ 1014 "id": cty.StringVal("i-02ae66f368e8518a9"), 1015 "ami": cty.StringVal("ami-STATIC"), 1016 "list_field": cty.NullVal(cty.List(cty.String)), 1017 }), 1018 After: cty.ObjectVal(map[string]cty.Value{ 1019 "id": cty.UnknownVal(cty.String), 1020 "ami": cty.StringVal("ami-STATIC"), 1021 "list_field": cty.ListVal([]cty.Value{ 1022 cty.StringVal("new-element"), 1023 }), 1024 }), 1025 Schema: &configschema.Block{ 1026 Attributes: map[string]*configschema.Attribute{ 1027 "id": {Type: cty.String, Optional: true, Computed: true}, 1028 "ami": {Type: cty.String, Optional: true}, 1029 "list_field": {Type: cty.List(cty.String), Optional: true}, 1030 }, 1031 }, 1032 RequiredReplace: cty.NewPathSet(), 1033 Tainted: false, 1034 ExpectedOutput: ` # test_instance.example will be updated in-place 1035 ~ resource "test_instance" "example" { 1036 ami = "ami-STATIC" 1037 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1038 + list_field = [ 1039 + "new-element", 1040 ] 1041 } 1042 `, 1043 }, 1044 "in-place update - first addition": { 1045 Action: plans.Update, 1046 Mode: addrs.ManagedResourceMode, 1047 Before: cty.ObjectVal(map[string]cty.Value{ 1048 "id": cty.StringVal("i-02ae66f368e8518a9"), 1049 "ami": cty.StringVal("ami-STATIC"), 1050 "list_field": cty.ListValEmpty(cty.String), 1051 }), 1052 After: cty.ObjectVal(map[string]cty.Value{ 1053 "id": cty.UnknownVal(cty.String), 1054 "ami": cty.StringVal("ami-STATIC"), 1055 "list_field": cty.ListVal([]cty.Value{ 1056 cty.StringVal("new-element"), 1057 }), 1058 }), 1059 Schema: &configschema.Block{ 1060 Attributes: map[string]*configschema.Attribute{ 1061 "id": {Type: cty.String, Optional: true, Computed: true}, 1062 "ami": {Type: cty.String, Optional: true}, 1063 "list_field": {Type: cty.List(cty.String), Optional: true}, 1064 }, 1065 }, 1066 RequiredReplace: cty.NewPathSet(), 1067 Tainted: false, 1068 ExpectedOutput: ` # test_instance.example will be updated in-place 1069 ~ resource "test_instance" "example" { 1070 ami = "ami-STATIC" 1071 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1072 ~ list_field = [ 1073 + "new-element", 1074 ] 1075 } 1076 `, 1077 }, 1078 "in-place update - insertion": { 1079 Action: plans.Update, 1080 Mode: addrs.ManagedResourceMode, 1081 Before: cty.ObjectVal(map[string]cty.Value{ 1082 "id": cty.StringVal("i-02ae66f368e8518a9"), 1083 "ami": cty.StringVal("ami-STATIC"), 1084 "list_field": cty.ListVal([]cty.Value{ 1085 cty.StringVal("aaaa"), 1086 cty.StringVal("cccc"), 1087 }), 1088 }), 1089 After: cty.ObjectVal(map[string]cty.Value{ 1090 "id": cty.UnknownVal(cty.String), 1091 "ami": cty.StringVal("ami-STATIC"), 1092 "list_field": cty.ListVal([]cty.Value{ 1093 cty.StringVal("aaaa"), 1094 cty.StringVal("bbbb"), 1095 cty.StringVal("cccc"), 1096 }), 1097 }), 1098 Schema: &configschema.Block{ 1099 Attributes: map[string]*configschema.Attribute{ 1100 "id": {Type: cty.String, Optional: true, Computed: true}, 1101 "ami": {Type: cty.String, Optional: true}, 1102 "list_field": {Type: cty.List(cty.String), Optional: true}, 1103 }, 1104 }, 1105 RequiredReplace: cty.NewPathSet(), 1106 Tainted: false, 1107 ExpectedOutput: ` # test_instance.example will be updated in-place 1108 ~ resource "test_instance" "example" { 1109 ami = "ami-STATIC" 1110 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1111 ~ list_field = [ 1112 "aaaa", 1113 + "bbbb", 1114 "cccc", 1115 ] 1116 } 1117 `, 1118 }, 1119 "force-new update - insertion": { 1120 Action: plans.DeleteThenCreate, 1121 Mode: addrs.ManagedResourceMode, 1122 Before: cty.ObjectVal(map[string]cty.Value{ 1123 "id": cty.StringVal("i-02ae66f368e8518a9"), 1124 "ami": cty.StringVal("ami-STATIC"), 1125 "list_field": cty.ListVal([]cty.Value{ 1126 cty.StringVal("aaaa"), 1127 cty.StringVal("cccc"), 1128 }), 1129 }), 1130 After: cty.ObjectVal(map[string]cty.Value{ 1131 "id": cty.UnknownVal(cty.String), 1132 "ami": cty.StringVal("ami-STATIC"), 1133 "list_field": cty.ListVal([]cty.Value{ 1134 cty.StringVal("aaaa"), 1135 cty.StringVal("bbbb"), 1136 cty.StringVal("cccc"), 1137 }), 1138 }), 1139 Schema: &configschema.Block{ 1140 Attributes: map[string]*configschema.Attribute{ 1141 "id": {Type: cty.String, Optional: true, Computed: true}, 1142 "ami": {Type: cty.String, Optional: true}, 1143 "list_field": {Type: cty.List(cty.String), Optional: true}, 1144 }, 1145 }, 1146 RequiredReplace: cty.NewPathSet(cty.Path{ 1147 cty.GetAttrStep{Name: "list_field"}, 1148 }), 1149 Tainted: false, 1150 ExpectedOutput: ` # test_instance.example must be replaced 1151 -/+ resource "test_instance" "example" { 1152 ami = "ami-STATIC" 1153 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1154 ~ list_field = [ # forces replacement 1155 "aaaa", 1156 + "bbbb", 1157 "cccc", 1158 ] 1159 } 1160 `, 1161 }, 1162 "in-place update - deletion": { 1163 Action: plans.Update, 1164 Mode: addrs.ManagedResourceMode, 1165 Before: cty.ObjectVal(map[string]cty.Value{ 1166 "id": cty.StringVal("i-02ae66f368e8518a9"), 1167 "ami": cty.StringVal("ami-STATIC"), 1168 "list_field": cty.ListVal([]cty.Value{ 1169 cty.StringVal("aaaa"), 1170 cty.StringVal("bbbb"), 1171 cty.StringVal("cccc"), 1172 }), 1173 }), 1174 After: cty.ObjectVal(map[string]cty.Value{ 1175 "id": cty.UnknownVal(cty.String), 1176 "ami": cty.StringVal("ami-STATIC"), 1177 "list_field": cty.ListVal([]cty.Value{ 1178 cty.StringVal("bbbb"), 1179 }), 1180 }), 1181 Schema: &configschema.Block{ 1182 Attributes: map[string]*configschema.Attribute{ 1183 "id": {Type: cty.String, Optional: true, Computed: true}, 1184 "ami": {Type: cty.String, Optional: true}, 1185 "list_field": {Type: cty.List(cty.String), Optional: true}, 1186 }, 1187 }, 1188 RequiredReplace: cty.NewPathSet(), 1189 Tainted: false, 1190 ExpectedOutput: ` # test_instance.example will be updated in-place 1191 ~ resource "test_instance" "example" { 1192 ami = "ami-STATIC" 1193 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1194 ~ list_field = [ 1195 - "aaaa", 1196 "bbbb", 1197 - "cccc", 1198 ] 1199 } 1200 `, 1201 }, 1202 "creation - empty list": { 1203 Action: plans.Create, 1204 Mode: addrs.ManagedResourceMode, 1205 Before: cty.NullVal(cty.EmptyObject), 1206 After: cty.ObjectVal(map[string]cty.Value{ 1207 "id": cty.UnknownVal(cty.String), 1208 "ami": cty.StringVal("ami-STATIC"), 1209 "list_field": cty.ListValEmpty(cty.String), 1210 }), 1211 Schema: &configschema.Block{ 1212 Attributes: map[string]*configschema.Attribute{ 1213 "id": {Type: cty.String, Optional: true, Computed: true}, 1214 "ami": {Type: cty.String, Optional: true}, 1215 "list_field": {Type: cty.List(cty.String), Optional: true}, 1216 }, 1217 }, 1218 RequiredReplace: cty.NewPathSet(), 1219 Tainted: false, 1220 ExpectedOutput: ` # test_instance.example will be created 1221 + resource "test_instance" "example" { 1222 + ami = "ami-STATIC" 1223 + id = (known after apply) 1224 + list_field = [] 1225 } 1226 `, 1227 }, 1228 "in-place update - full to empty": { 1229 Action: plans.Update, 1230 Mode: addrs.ManagedResourceMode, 1231 Before: cty.ObjectVal(map[string]cty.Value{ 1232 "id": cty.StringVal("i-02ae66f368e8518a9"), 1233 "ami": cty.StringVal("ami-STATIC"), 1234 "list_field": cty.ListVal([]cty.Value{ 1235 cty.StringVal("aaaa"), 1236 cty.StringVal("bbbb"), 1237 cty.StringVal("cccc"), 1238 }), 1239 }), 1240 After: cty.ObjectVal(map[string]cty.Value{ 1241 "id": cty.UnknownVal(cty.String), 1242 "ami": cty.StringVal("ami-STATIC"), 1243 "list_field": cty.ListValEmpty(cty.String), 1244 }), 1245 Schema: &configschema.Block{ 1246 Attributes: map[string]*configschema.Attribute{ 1247 "id": {Type: cty.String, Optional: true, Computed: true}, 1248 "ami": {Type: cty.String, Optional: true}, 1249 "list_field": {Type: cty.List(cty.String), Optional: true}, 1250 }, 1251 }, 1252 RequiredReplace: cty.NewPathSet(), 1253 Tainted: false, 1254 ExpectedOutput: ` # test_instance.example will be updated in-place 1255 ~ resource "test_instance" "example" { 1256 ami = "ami-STATIC" 1257 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1258 ~ list_field = [ 1259 - "aaaa", 1260 - "bbbb", 1261 - "cccc", 1262 ] 1263 } 1264 `, 1265 }, 1266 "in-place update - null to empty": { 1267 Action: plans.Update, 1268 Mode: addrs.ManagedResourceMode, 1269 Before: cty.ObjectVal(map[string]cty.Value{ 1270 "id": cty.StringVal("i-02ae66f368e8518a9"), 1271 "ami": cty.StringVal("ami-STATIC"), 1272 "list_field": cty.NullVal(cty.List(cty.String)), 1273 }), 1274 After: cty.ObjectVal(map[string]cty.Value{ 1275 "id": cty.UnknownVal(cty.String), 1276 "ami": cty.StringVal("ami-STATIC"), 1277 "list_field": cty.ListValEmpty(cty.String), 1278 }), 1279 Schema: &configschema.Block{ 1280 Attributes: map[string]*configschema.Attribute{ 1281 "id": {Type: cty.String, Optional: true, Computed: true}, 1282 "ami": {Type: cty.String, Optional: true}, 1283 "list_field": {Type: cty.List(cty.String), Optional: true}, 1284 }, 1285 }, 1286 RequiredReplace: cty.NewPathSet(), 1287 Tainted: false, 1288 ExpectedOutput: ` # test_instance.example will be updated in-place 1289 ~ resource "test_instance" "example" { 1290 ami = "ami-STATIC" 1291 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1292 + list_field = [] 1293 } 1294 `, 1295 }, 1296 "update to unknown element": { 1297 Action: plans.Update, 1298 Mode: addrs.ManagedResourceMode, 1299 Before: cty.ObjectVal(map[string]cty.Value{ 1300 "id": cty.StringVal("i-02ae66f368e8518a9"), 1301 "ami": cty.StringVal("ami-STATIC"), 1302 "list_field": cty.ListVal([]cty.Value{ 1303 cty.StringVal("aaaa"), 1304 cty.StringVal("bbbb"), 1305 cty.StringVal("cccc"), 1306 }), 1307 }), 1308 After: cty.ObjectVal(map[string]cty.Value{ 1309 "id": cty.UnknownVal(cty.String), 1310 "ami": cty.StringVal("ami-STATIC"), 1311 "list_field": cty.ListVal([]cty.Value{ 1312 cty.StringVal("aaaa"), 1313 cty.UnknownVal(cty.String), 1314 cty.StringVal("cccc"), 1315 }), 1316 }), 1317 Schema: &configschema.Block{ 1318 Attributes: map[string]*configschema.Attribute{ 1319 "id": {Type: cty.String, Optional: true, Computed: true}, 1320 "ami": {Type: cty.String, Optional: true}, 1321 "list_field": {Type: cty.List(cty.String), Optional: true}, 1322 }, 1323 }, 1324 RequiredReplace: cty.NewPathSet(), 1325 Tainted: false, 1326 ExpectedOutput: ` # test_instance.example will be updated in-place 1327 ~ resource "test_instance" "example" { 1328 ami = "ami-STATIC" 1329 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1330 ~ list_field = [ 1331 "aaaa", 1332 - "bbbb", 1333 + (known after apply), 1334 "cccc", 1335 ] 1336 } 1337 `, 1338 }, 1339 "update - two new unknown elements": { 1340 Action: plans.Update, 1341 Mode: addrs.ManagedResourceMode, 1342 Before: cty.ObjectVal(map[string]cty.Value{ 1343 "id": cty.StringVal("i-02ae66f368e8518a9"), 1344 "ami": cty.StringVal("ami-STATIC"), 1345 "list_field": cty.ListVal([]cty.Value{ 1346 cty.StringVal("aaaa"), 1347 cty.StringVal("bbbb"), 1348 cty.StringVal("cccc"), 1349 }), 1350 }), 1351 After: cty.ObjectVal(map[string]cty.Value{ 1352 "id": cty.UnknownVal(cty.String), 1353 "ami": cty.StringVal("ami-STATIC"), 1354 "list_field": cty.ListVal([]cty.Value{ 1355 cty.StringVal("aaaa"), 1356 cty.UnknownVal(cty.String), 1357 cty.UnknownVal(cty.String), 1358 cty.StringVal("cccc"), 1359 }), 1360 }), 1361 Schema: &configschema.Block{ 1362 Attributes: map[string]*configschema.Attribute{ 1363 "id": {Type: cty.String, Optional: true, Computed: true}, 1364 "ami": {Type: cty.String, Optional: true}, 1365 "list_field": {Type: cty.List(cty.String), Optional: true}, 1366 }, 1367 }, 1368 RequiredReplace: cty.NewPathSet(), 1369 Tainted: false, 1370 ExpectedOutput: ` # test_instance.example will be updated in-place 1371 ~ resource "test_instance" "example" { 1372 ami = "ami-STATIC" 1373 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1374 ~ list_field = [ 1375 "aaaa", 1376 - "bbbb", 1377 + (known after apply), 1378 + (known after apply), 1379 "cccc", 1380 ] 1381 } 1382 `, 1383 }, 1384 } 1385 runTestCases(t, testCases) 1386 } 1387 1388 func TestResourceChange_primitiveSet(t *testing.T) { 1389 testCases := map[string]testCase{ 1390 "in-place update - creation": { 1391 Action: plans.Update, 1392 Mode: addrs.ManagedResourceMode, 1393 Before: cty.ObjectVal(map[string]cty.Value{ 1394 "id": cty.StringVal("i-02ae66f368e8518a9"), 1395 "ami": cty.StringVal("ami-STATIC"), 1396 "set_field": cty.NullVal(cty.Set(cty.String)), 1397 }), 1398 After: cty.ObjectVal(map[string]cty.Value{ 1399 "id": cty.UnknownVal(cty.String), 1400 "ami": cty.StringVal("ami-STATIC"), 1401 "set_field": cty.SetVal([]cty.Value{ 1402 cty.StringVal("new-element"), 1403 }), 1404 }), 1405 Schema: &configschema.Block{ 1406 Attributes: map[string]*configschema.Attribute{ 1407 "id": {Type: cty.String, Optional: true, Computed: true}, 1408 "ami": {Type: cty.String, Optional: true}, 1409 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1410 }, 1411 }, 1412 RequiredReplace: cty.NewPathSet(), 1413 Tainted: false, 1414 ExpectedOutput: ` # test_instance.example will be updated in-place 1415 ~ resource "test_instance" "example" { 1416 ami = "ami-STATIC" 1417 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1418 + set_field = [ 1419 + "new-element", 1420 ] 1421 } 1422 `, 1423 }, 1424 "in-place update - first insertion": { 1425 Action: plans.Update, 1426 Mode: addrs.ManagedResourceMode, 1427 Before: cty.ObjectVal(map[string]cty.Value{ 1428 "id": cty.StringVal("i-02ae66f368e8518a9"), 1429 "ami": cty.StringVal("ami-STATIC"), 1430 "set_field": cty.SetValEmpty(cty.String), 1431 }), 1432 After: cty.ObjectVal(map[string]cty.Value{ 1433 "id": cty.UnknownVal(cty.String), 1434 "ami": cty.StringVal("ami-STATIC"), 1435 "set_field": cty.SetVal([]cty.Value{ 1436 cty.StringVal("new-element"), 1437 }), 1438 }), 1439 Schema: &configschema.Block{ 1440 Attributes: map[string]*configschema.Attribute{ 1441 "id": {Type: cty.String, Optional: true, Computed: true}, 1442 "ami": {Type: cty.String, Optional: true}, 1443 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1444 }, 1445 }, 1446 RequiredReplace: cty.NewPathSet(), 1447 Tainted: false, 1448 ExpectedOutput: ` # test_instance.example will be updated in-place 1449 ~ resource "test_instance" "example" { 1450 ami = "ami-STATIC" 1451 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1452 ~ set_field = [ 1453 + "new-element", 1454 ] 1455 } 1456 `, 1457 }, 1458 "in-place update - insertion": { 1459 Action: plans.Update, 1460 Mode: addrs.ManagedResourceMode, 1461 Before: cty.ObjectVal(map[string]cty.Value{ 1462 "id": cty.StringVal("i-02ae66f368e8518a9"), 1463 "ami": cty.StringVal("ami-STATIC"), 1464 "set_field": cty.SetVal([]cty.Value{ 1465 cty.StringVal("aaaa"), 1466 cty.StringVal("cccc"), 1467 }), 1468 }), 1469 After: cty.ObjectVal(map[string]cty.Value{ 1470 "id": cty.UnknownVal(cty.String), 1471 "ami": cty.StringVal("ami-STATIC"), 1472 "set_field": cty.SetVal([]cty.Value{ 1473 cty.StringVal("aaaa"), 1474 cty.StringVal("bbbb"), 1475 cty.StringVal("cccc"), 1476 }), 1477 }), 1478 Schema: &configschema.Block{ 1479 Attributes: map[string]*configschema.Attribute{ 1480 "id": {Type: cty.String, Optional: true, Computed: true}, 1481 "ami": {Type: cty.String, Optional: true}, 1482 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1483 }, 1484 }, 1485 RequiredReplace: cty.NewPathSet(), 1486 Tainted: false, 1487 ExpectedOutput: ` # test_instance.example will be updated in-place 1488 ~ resource "test_instance" "example" { 1489 ami = "ami-STATIC" 1490 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1491 ~ set_field = [ 1492 "aaaa", 1493 + "bbbb", 1494 "cccc", 1495 ] 1496 } 1497 `, 1498 }, 1499 "force-new update - insertion": { 1500 Action: plans.DeleteThenCreate, 1501 Mode: addrs.ManagedResourceMode, 1502 Before: cty.ObjectVal(map[string]cty.Value{ 1503 "id": cty.StringVal("i-02ae66f368e8518a9"), 1504 "ami": cty.StringVal("ami-STATIC"), 1505 "set_field": cty.SetVal([]cty.Value{ 1506 cty.StringVal("aaaa"), 1507 cty.StringVal("cccc"), 1508 }), 1509 }), 1510 After: cty.ObjectVal(map[string]cty.Value{ 1511 "id": cty.UnknownVal(cty.String), 1512 "ami": cty.StringVal("ami-STATIC"), 1513 "set_field": cty.SetVal([]cty.Value{ 1514 cty.StringVal("aaaa"), 1515 cty.StringVal("bbbb"), 1516 cty.StringVal("cccc"), 1517 }), 1518 }), 1519 Schema: &configschema.Block{ 1520 Attributes: map[string]*configschema.Attribute{ 1521 "id": {Type: cty.String, Optional: true, Computed: true}, 1522 "ami": {Type: cty.String, Optional: true}, 1523 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1524 }, 1525 }, 1526 RequiredReplace: cty.NewPathSet(cty.Path{ 1527 cty.GetAttrStep{Name: "set_field"}, 1528 }), 1529 Tainted: false, 1530 ExpectedOutput: ` # test_instance.example must be replaced 1531 -/+ resource "test_instance" "example" { 1532 ami = "ami-STATIC" 1533 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1534 ~ set_field = [ # forces replacement 1535 "aaaa", 1536 + "bbbb", 1537 "cccc", 1538 ] 1539 } 1540 `, 1541 }, 1542 "in-place update - deletion": { 1543 Action: plans.Update, 1544 Mode: addrs.ManagedResourceMode, 1545 Before: cty.ObjectVal(map[string]cty.Value{ 1546 "id": cty.StringVal("i-02ae66f368e8518a9"), 1547 "ami": cty.StringVal("ami-STATIC"), 1548 "set_field": cty.SetVal([]cty.Value{ 1549 cty.StringVal("aaaa"), 1550 cty.StringVal("bbbb"), 1551 cty.StringVal("cccc"), 1552 }), 1553 }), 1554 After: cty.ObjectVal(map[string]cty.Value{ 1555 "id": cty.UnknownVal(cty.String), 1556 "ami": cty.StringVal("ami-STATIC"), 1557 "set_field": cty.SetVal([]cty.Value{ 1558 cty.StringVal("bbbb"), 1559 }), 1560 }), 1561 Schema: &configschema.Block{ 1562 Attributes: map[string]*configschema.Attribute{ 1563 "id": {Type: cty.String, Optional: true, Computed: true}, 1564 "ami": {Type: cty.String, Optional: true}, 1565 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1566 }, 1567 }, 1568 RequiredReplace: cty.NewPathSet(), 1569 Tainted: false, 1570 ExpectedOutput: ` # test_instance.example will be updated in-place 1571 ~ resource "test_instance" "example" { 1572 ami = "ami-STATIC" 1573 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1574 ~ set_field = [ 1575 - "aaaa", 1576 "bbbb", 1577 - "cccc", 1578 ] 1579 } 1580 `, 1581 }, 1582 "creation - empty set": { 1583 Action: plans.Create, 1584 Mode: addrs.ManagedResourceMode, 1585 Before: cty.NullVal(cty.EmptyObject), 1586 After: cty.ObjectVal(map[string]cty.Value{ 1587 "id": cty.UnknownVal(cty.String), 1588 "ami": cty.StringVal("ami-STATIC"), 1589 "set_field": cty.SetValEmpty(cty.String), 1590 }), 1591 Schema: &configschema.Block{ 1592 Attributes: map[string]*configschema.Attribute{ 1593 "id": {Type: cty.String, Optional: true, Computed: true}, 1594 "ami": {Type: cty.String, Optional: true}, 1595 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1596 }, 1597 }, 1598 RequiredReplace: cty.NewPathSet(), 1599 Tainted: false, 1600 ExpectedOutput: ` # test_instance.example will be created 1601 + resource "test_instance" "example" { 1602 + ami = "ami-STATIC" 1603 + id = (known after apply) 1604 + set_field = [] 1605 } 1606 `, 1607 }, 1608 "in-place update - full to empty set": { 1609 Action: plans.Update, 1610 Mode: addrs.ManagedResourceMode, 1611 Before: cty.ObjectVal(map[string]cty.Value{ 1612 "id": cty.StringVal("i-02ae66f368e8518a9"), 1613 "ami": cty.StringVal("ami-STATIC"), 1614 "set_field": cty.SetVal([]cty.Value{ 1615 cty.StringVal("aaaa"), 1616 cty.StringVal("bbbb"), 1617 }), 1618 }), 1619 After: cty.ObjectVal(map[string]cty.Value{ 1620 "id": cty.UnknownVal(cty.String), 1621 "ami": cty.StringVal("ami-STATIC"), 1622 "set_field": cty.SetValEmpty(cty.String), 1623 }), 1624 Schema: &configschema.Block{ 1625 Attributes: map[string]*configschema.Attribute{ 1626 "id": {Type: cty.String, Optional: true, Computed: true}, 1627 "ami": {Type: cty.String, Optional: true}, 1628 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1629 }, 1630 }, 1631 RequiredReplace: cty.NewPathSet(), 1632 ExpectedOutput: ` # test_instance.example will be updated in-place 1633 ~ resource "test_instance" "example" { 1634 ami = "ami-STATIC" 1635 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1636 ~ set_field = [ 1637 - "aaaa", 1638 - "bbbb", 1639 ] 1640 } 1641 `, 1642 }, 1643 "in-place update - null to empty set": { 1644 Action: plans.Update, 1645 Mode: addrs.ManagedResourceMode, 1646 Before: cty.ObjectVal(map[string]cty.Value{ 1647 "id": cty.StringVal("i-02ae66f368e8518a9"), 1648 "ami": cty.StringVal("ami-STATIC"), 1649 "set_field": cty.NullVal(cty.Set(cty.String)), 1650 }), 1651 After: cty.ObjectVal(map[string]cty.Value{ 1652 "id": cty.UnknownVal(cty.String), 1653 "ami": cty.StringVal("ami-STATIC"), 1654 "set_field": cty.SetValEmpty(cty.String), 1655 }), 1656 Schema: &configschema.Block{ 1657 Attributes: map[string]*configschema.Attribute{ 1658 "id": {Type: cty.String, Optional: true, Computed: true}, 1659 "ami": {Type: cty.String, Optional: true}, 1660 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1661 }, 1662 }, 1663 RequiredReplace: cty.NewPathSet(), 1664 Tainted: false, 1665 ExpectedOutput: ` # test_instance.example will be updated in-place 1666 ~ resource "test_instance" "example" { 1667 ami = "ami-STATIC" 1668 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1669 + set_field = [] 1670 } 1671 `, 1672 }, 1673 "in-place update to unknown": { 1674 Action: plans.Update, 1675 Mode: addrs.ManagedResourceMode, 1676 Before: cty.ObjectVal(map[string]cty.Value{ 1677 "id": cty.StringVal("i-02ae66f368e8518a9"), 1678 "ami": cty.StringVal("ami-STATIC"), 1679 "set_field": cty.SetVal([]cty.Value{ 1680 cty.StringVal("aaaa"), 1681 cty.StringVal("bbbb"), 1682 }), 1683 }), 1684 After: cty.ObjectVal(map[string]cty.Value{ 1685 "id": cty.UnknownVal(cty.String), 1686 "ami": cty.StringVal("ami-STATIC"), 1687 "set_field": cty.UnknownVal(cty.Set(cty.String)), 1688 }), 1689 Schema: &configschema.Block{ 1690 Attributes: map[string]*configschema.Attribute{ 1691 "id": {Type: cty.String, Optional: true, Computed: true}, 1692 "ami": {Type: cty.String, Optional: true}, 1693 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1694 }, 1695 }, 1696 RequiredReplace: cty.NewPathSet(), 1697 Tainted: false, 1698 ExpectedOutput: ` # test_instance.example will be updated in-place 1699 ~ resource "test_instance" "example" { 1700 ami = "ami-STATIC" 1701 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1702 ~ set_field = [ 1703 - "aaaa", 1704 - "bbbb", 1705 ] -> (known after apply) 1706 } 1707 `, 1708 }, 1709 "in-place update to unknown element": { 1710 Action: plans.Update, 1711 Mode: addrs.ManagedResourceMode, 1712 Before: cty.ObjectVal(map[string]cty.Value{ 1713 "id": cty.StringVal("i-02ae66f368e8518a9"), 1714 "ami": cty.StringVal("ami-STATIC"), 1715 "set_field": cty.SetVal([]cty.Value{ 1716 cty.StringVal("aaaa"), 1717 cty.StringVal("bbbb"), 1718 }), 1719 }), 1720 After: cty.ObjectVal(map[string]cty.Value{ 1721 "id": cty.UnknownVal(cty.String), 1722 "ami": cty.StringVal("ami-STATIC"), 1723 "set_field": cty.SetVal([]cty.Value{ 1724 cty.StringVal("aaaa"), 1725 cty.UnknownVal(cty.String), 1726 }), 1727 }), 1728 Schema: &configschema.Block{ 1729 Attributes: map[string]*configschema.Attribute{ 1730 "id": {Type: cty.String, Optional: true, Computed: true}, 1731 "ami": {Type: cty.String, Optional: true}, 1732 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1733 }, 1734 }, 1735 RequiredReplace: cty.NewPathSet(), 1736 Tainted: false, 1737 ExpectedOutput: ` # test_instance.example will be updated in-place 1738 ~ resource "test_instance" "example" { 1739 ami = "ami-STATIC" 1740 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1741 ~ set_field = [ 1742 "aaaa", 1743 - "bbbb", 1744 ~ (known after apply), 1745 ] 1746 } 1747 `, 1748 }, 1749 } 1750 runTestCases(t, testCases) 1751 } 1752 1753 func TestResourceChange_map(t *testing.T) { 1754 testCases := map[string]testCase{ 1755 "in-place update - creation": { 1756 Action: plans.Update, 1757 Mode: addrs.ManagedResourceMode, 1758 Before: cty.ObjectVal(map[string]cty.Value{ 1759 "id": cty.StringVal("i-02ae66f368e8518a9"), 1760 "ami": cty.StringVal("ami-STATIC"), 1761 "map_field": cty.NullVal(cty.Map(cty.String)), 1762 }), 1763 After: cty.ObjectVal(map[string]cty.Value{ 1764 "id": cty.UnknownVal(cty.String), 1765 "ami": cty.StringVal("ami-STATIC"), 1766 "map_field": cty.MapVal(map[string]cty.Value{ 1767 "new-key": cty.StringVal("new-element"), 1768 }), 1769 }), 1770 Schema: &configschema.Block{ 1771 Attributes: map[string]*configschema.Attribute{ 1772 "id": {Type: cty.String, Optional: true, Computed: true}, 1773 "ami": {Type: cty.String, Optional: true}, 1774 "map_field": {Type: cty.Map(cty.String), Optional: true}, 1775 }, 1776 }, 1777 RequiredReplace: cty.NewPathSet(), 1778 Tainted: false, 1779 ExpectedOutput: ` # test_instance.example will be updated in-place 1780 ~ resource "test_instance" "example" { 1781 ami = "ami-STATIC" 1782 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1783 + map_field = { 1784 + "new-key" = "new-element" 1785 } 1786 } 1787 `, 1788 }, 1789 "in-place update - first insertion": { 1790 Action: plans.Update, 1791 Mode: addrs.ManagedResourceMode, 1792 Before: cty.ObjectVal(map[string]cty.Value{ 1793 "id": cty.StringVal("i-02ae66f368e8518a9"), 1794 "ami": cty.StringVal("ami-STATIC"), 1795 "map_field": cty.MapValEmpty(cty.String), 1796 }), 1797 After: cty.ObjectVal(map[string]cty.Value{ 1798 "id": cty.UnknownVal(cty.String), 1799 "ami": cty.StringVal("ami-STATIC"), 1800 "map_field": cty.MapVal(map[string]cty.Value{ 1801 "new-key": cty.StringVal("new-element"), 1802 }), 1803 }), 1804 Schema: &configschema.Block{ 1805 Attributes: map[string]*configschema.Attribute{ 1806 "id": {Type: cty.String, Optional: true, Computed: true}, 1807 "ami": {Type: cty.String, Optional: true}, 1808 "map_field": {Type: cty.Map(cty.String), Optional: true}, 1809 }, 1810 }, 1811 RequiredReplace: cty.NewPathSet(), 1812 Tainted: false, 1813 ExpectedOutput: ` # test_instance.example will be updated in-place 1814 ~ resource "test_instance" "example" { 1815 ami = "ami-STATIC" 1816 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1817 ~ map_field = { 1818 + "new-key" = "new-element" 1819 } 1820 } 1821 `, 1822 }, 1823 "in-place update - insertion": { 1824 Action: plans.Update, 1825 Mode: addrs.ManagedResourceMode, 1826 Before: cty.ObjectVal(map[string]cty.Value{ 1827 "id": cty.StringVal("i-02ae66f368e8518a9"), 1828 "ami": cty.StringVal("ami-STATIC"), 1829 "map_field": cty.MapVal(map[string]cty.Value{ 1830 "a": cty.StringVal("aaaa"), 1831 "c": cty.StringVal("cccc"), 1832 }), 1833 }), 1834 After: cty.ObjectVal(map[string]cty.Value{ 1835 "id": cty.UnknownVal(cty.String), 1836 "ami": cty.StringVal("ami-STATIC"), 1837 "map_field": cty.MapVal(map[string]cty.Value{ 1838 "a": cty.StringVal("aaaa"), 1839 "b": cty.StringVal("bbbb"), 1840 "c": cty.StringVal("cccc"), 1841 }), 1842 }), 1843 Schema: &configschema.Block{ 1844 Attributes: map[string]*configschema.Attribute{ 1845 "id": {Type: cty.String, Optional: true, Computed: true}, 1846 "ami": {Type: cty.String, Optional: true}, 1847 "map_field": {Type: cty.Map(cty.String), Optional: true}, 1848 }, 1849 }, 1850 RequiredReplace: cty.NewPathSet(), 1851 Tainted: false, 1852 ExpectedOutput: ` # test_instance.example will be updated in-place 1853 ~ resource "test_instance" "example" { 1854 ami = "ami-STATIC" 1855 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1856 ~ map_field = { 1857 "a" = "aaaa" 1858 + "b" = "bbbb" 1859 "c" = "cccc" 1860 } 1861 } 1862 `, 1863 }, 1864 "force-new update - insertion": { 1865 Action: plans.DeleteThenCreate, 1866 Mode: addrs.ManagedResourceMode, 1867 Before: cty.ObjectVal(map[string]cty.Value{ 1868 "id": cty.StringVal("i-02ae66f368e8518a9"), 1869 "ami": cty.StringVal("ami-STATIC"), 1870 "map_field": cty.MapVal(map[string]cty.Value{ 1871 "a": cty.StringVal("aaaa"), 1872 "c": cty.StringVal("cccc"), 1873 }), 1874 }), 1875 After: cty.ObjectVal(map[string]cty.Value{ 1876 "id": cty.UnknownVal(cty.String), 1877 "ami": cty.StringVal("ami-STATIC"), 1878 "map_field": cty.MapVal(map[string]cty.Value{ 1879 "a": cty.StringVal("aaaa"), 1880 "b": cty.StringVal("bbbb"), 1881 "c": cty.StringVal("cccc"), 1882 }), 1883 }), 1884 Schema: &configschema.Block{ 1885 Attributes: map[string]*configschema.Attribute{ 1886 "id": {Type: cty.String, Optional: true, Computed: true}, 1887 "ami": {Type: cty.String, Optional: true}, 1888 "map_field": {Type: cty.Map(cty.String), Optional: true}, 1889 }, 1890 }, 1891 RequiredReplace: cty.NewPathSet(cty.Path{ 1892 cty.GetAttrStep{Name: "map_field"}, 1893 }), 1894 Tainted: false, 1895 ExpectedOutput: ` # test_instance.example must be replaced 1896 -/+ resource "test_instance" "example" { 1897 ami = "ami-STATIC" 1898 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1899 ~ map_field = { # forces replacement 1900 "a" = "aaaa" 1901 + "b" = "bbbb" 1902 "c" = "cccc" 1903 } 1904 } 1905 `, 1906 }, 1907 "in-place update - deletion": { 1908 Action: plans.Update, 1909 Mode: addrs.ManagedResourceMode, 1910 Before: cty.ObjectVal(map[string]cty.Value{ 1911 "id": cty.StringVal("i-02ae66f368e8518a9"), 1912 "ami": cty.StringVal("ami-STATIC"), 1913 "map_field": cty.MapVal(map[string]cty.Value{ 1914 "a": cty.StringVal("aaaa"), 1915 "b": cty.StringVal("bbbb"), 1916 "c": cty.StringVal("cccc"), 1917 }), 1918 }), 1919 After: cty.ObjectVal(map[string]cty.Value{ 1920 "id": cty.UnknownVal(cty.String), 1921 "ami": cty.StringVal("ami-STATIC"), 1922 "map_field": cty.MapVal(map[string]cty.Value{ 1923 "b": cty.StringVal("bbbb"), 1924 }), 1925 }), 1926 Schema: &configschema.Block{ 1927 Attributes: map[string]*configschema.Attribute{ 1928 "id": {Type: cty.String, Optional: true, Computed: true}, 1929 "ami": {Type: cty.String, Optional: true}, 1930 "map_field": {Type: cty.Map(cty.String), Optional: true}, 1931 }, 1932 }, 1933 RequiredReplace: cty.NewPathSet(), 1934 Tainted: false, 1935 ExpectedOutput: ` # test_instance.example will be updated in-place 1936 ~ resource "test_instance" "example" { 1937 ami = "ami-STATIC" 1938 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1939 ~ map_field = { 1940 - "a" = "aaaa" -> null 1941 "b" = "bbbb" 1942 - "c" = "cccc" -> null 1943 } 1944 } 1945 `, 1946 }, 1947 "creation - empty": { 1948 Action: plans.Create, 1949 Mode: addrs.ManagedResourceMode, 1950 Before: cty.NullVal(cty.EmptyObject), 1951 After: cty.ObjectVal(map[string]cty.Value{ 1952 "id": cty.UnknownVal(cty.String), 1953 "ami": cty.StringVal("ami-STATIC"), 1954 "map_field": cty.MapValEmpty(cty.String), 1955 }), 1956 Schema: &configschema.Block{ 1957 Attributes: map[string]*configschema.Attribute{ 1958 "id": {Type: cty.String, Optional: true, Computed: true}, 1959 "ami": {Type: cty.String, Optional: true}, 1960 "map_field": {Type: cty.Map(cty.String), Optional: true}, 1961 }, 1962 }, 1963 RequiredReplace: cty.NewPathSet(), 1964 Tainted: false, 1965 ExpectedOutput: ` # test_instance.example will be created 1966 + resource "test_instance" "example" { 1967 + ami = "ami-STATIC" 1968 + id = (known after apply) 1969 + map_field = {} 1970 } 1971 `, 1972 }, 1973 "update to unknown element": { 1974 Action: plans.Update, 1975 Mode: addrs.ManagedResourceMode, 1976 Before: cty.ObjectVal(map[string]cty.Value{ 1977 "id": cty.StringVal("i-02ae66f368e8518a9"), 1978 "ami": cty.StringVal("ami-STATIC"), 1979 "map_field": cty.MapVal(map[string]cty.Value{ 1980 "a": cty.StringVal("aaaa"), 1981 "b": cty.StringVal("bbbb"), 1982 "c": cty.StringVal("cccc"), 1983 }), 1984 }), 1985 After: cty.ObjectVal(map[string]cty.Value{ 1986 "id": cty.UnknownVal(cty.String), 1987 "ami": cty.StringVal("ami-STATIC"), 1988 "map_field": cty.MapVal(map[string]cty.Value{ 1989 "a": cty.StringVal("aaaa"), 1990 "b": cty.UnknownVal(cty.String), 1991 "c": cty.StringVal("cccc"), 1992 }), 1993 }), 1994 Schema: &configschema.Block{ 1995 Attributes: map[string]*configschema.Attribute{ 1996 "id": {Type: cty.String, Optional: true, Computed: true}, 1997 "ami": {Type: cty.String, Optional: true}, 1998 "map_field": {Type: cty.Map(cty.String), Optional: true}, 1999 }, 2000 }, 2001 RequiredReplace: cty.NewPathSet(), 2002 Tainted: false, 2003 ExpectedOutput: ` # test_instance.example will be updated in-place 2004 ~ resource "test_instance" "example" { 2005 ami = "ami-STATIC" 2006 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2007 ~ map_field = { 2008 "a" = "aaaa" 2009 ~ "b" = "bbbb" -> (known after apply) 2010 "c" = "cccc" 2011 } 2012 } 2013 `, 2014 }, 2015 } 2016 runTestCases(t, testCases) 2017 } 2018 2019 func TestResourceChange_nestedList(t *testing.T) { 2020 testCases := map[string]testCase{ 2021 "in-place update - equal": { 2022 Action: plans.Update, 2023 Mode: addrs.ManagedResourceMode, 2024 Before: cty.ObjectVal(map[string]cty.Value{ 2025 "id": cty.StringVal("i-02ae66f368e8518a9"), 2026 "ami": cty.StringVal("ami-BEFORE"), 2027 "root_block_device": cty.ListVal([]cty.Value{ 2028 cty.ObjectVal(map[string]cty.Value{ 2029 "volume_type": cty.StringVal("gp2"), 2030 }), 2031 }), 2032 }), 2033 After: cty.ObjectVal(map[string]cty.Value{ 2034 "id": cty.StringVal("i-02ae66f368e8518a9"), 2035 "ami": cty.StringVal("ami-AFTER"), 2036 "root_block_device": cty.ListVal([]cty.Value{ 2037 cty.ObjectVal(map[string]cty.Value{ 2038 "volume_type": cty.StringVal("gp2"), 2039 }), 2040 }), 2041 }), 2042 RequiredReplace: cty.NewPathSet(), 2043 Tainted: false, 2044 Schema: &configschema.Block{ 2045 Attributes: map[string]*configschema.Attribute{ 2046 "id": {Type: cty.String, Optional: true, Computed: true}, 2047 "ami": {Type: cty.String, Optional: true}, 2048 }, 2049 BlockTypes: map[string]*configschema.NestedBlock{ 2050 "root_block_device": { 2051 Block: configschema.Block{ 2052 Attributes: map[string]*configschema.Attribute{ 2053 "volume_type": { 2054 Type: cty.String, 2055 Optional: true, 2056 Computed: true, 2057 }, 2058 }, 2059 }, 2060 Nesting: configschema.NestingList, 2061 }, 2062 }, 2063 }, 2064 ExpectedOutput: ` # test_instance.example will be updated in-place 2065 ~ resource "test_instance" "example" { 2066 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2067 id = "i-02ae66f368e8518a9" 2068 2069 root_block_device { 2070 volume_type = "gp2" 2071 } 2072 } 2073 `, 2074 }, 2075 "in-place update - creation": { 2076 Action: plans.Update, 2077 Mode: addrs.ManagedResourceMode, 2078 Before: cty.ObjectVal(map[string]cty.Value{ 2079 "id": cty.StringVal("i-02ae66f368e8518a9"), 2080 "ami": cty.StringVal("ami-BEFORE"), 2081 "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2082 "volume_type": cty.String, 2083 })), 2084 }), 2085 After: cty.ObjectVal(map[string]cty.Value{ 2086 "id": cty.StringVal("i-02ae66f368e8518a9"), 2087 "ami": cty.StringVal("ami-AFTER"), 2088 "root_block_device": cty.ListVal([]cty.Value{ 2089 cty.ObjectVal(map[string]cty.Value{ 2090 "volume_type": cty.NullVal(cty.String), 2091 }), 2092 }), 2093 }), 2094 RequiredReplace: cty.NewPathSet(), 2095 Tainted: false, 2096 Schema: &configschema.Block{ 2097 Attributes: map[string]*configschema.Attribute{ 2098 "id": {Type: cty.String, Optional: true, Computed: true}, 2099 "ami": {Type: cty.String, Optional: true}, 2100 }, 2101 BlockTypes: map[string]*configschema.NestedBlock{ 2102 "root_block_device": { 2103 Block: configschema.Block{ 2104 Attributes: map[string]*configschema.Attribute{ 2105 "volume_type": { 2106 Type: cty.String, 2107 Optional: true, 2108 Computed: true, 2109 }, 2110 }, 2111 }, 2112 Nesting: configschema.NestingList, 2113 }, 2114 }, 2115 }, 2116 ExpectedOutput: ` # test_instance.example will be updated in-place 2117 ~ resource "test_instance" "example" { 2118 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2119 id = "i-02ae66f368e8518a9" 2120 2121 + root_block_device {} 2122 } 2123 `, 2124 }, 2125 "in-place update - first insertion": { 2126 Action: plans.Update, 2127 Mode: addrs.ManagedResourceMode, 2128 Before: cty.ObjectVal(map[string]cty.Value{ 2129 "id": cty.StringVal("i-02ae66f368e8518a9"), 2130 "ami": cty.StringVal("ami-BEFORE"), 2131 "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2132 "volume_type": cty.String, 2133 })), 2134 }), 2135 After: cty.ObjectVal(map[string]cty.Value{ 2136 "id": cty.StringVal("i-02ae66f368e8518a9"), 2137 "ami": cty.StringVal("ami-AFTER"), 2138 "root_block_device": cty.ListVal([]cty.Value{ 2139 cty.ObjectVal(map[string]cty.Value{ 2140 "volume_type": cty.StringVal("gp2"), 2141 }), 2142 }), 2143 }), 2144 RequiredReplace: cty.NewPathSet(), 2145 Tainted: false, 2146 Schema: &configschema.Block{ 2147 Attributes: map[string]*configschema.Attribute{ 2148 "id": {Type: cty.String, Optional: true, Computed: true}, 2149 "ami": {Type: cty.String, Optional: true}, 2150 }, 2151 BlockTypes: map[string]*configschema.NestedBlock{ 2152 "root_block_device": { 2153 Block: configschema.Block{ 2154 Attributes: map[string]*configschema.Attribute{ 2155 "volume_type": { 2156 Type: cty.String, 2157 Optional: true, 2158 Computed: true, 2159 }, 2160 }, 2161 }, 2162 Nesting: configschema.NestingList, 2163 }, 2164 }, 2165 }, 2166 ExpectedOutput: ` # test_instance.example will be updated in-place 2167 ~ resource "test_instance" "example" { 2168 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2169 id = "i-02ae66f368e8518a9" 2170 2171 + root_block_device { 2172 + volume_type = "gp2" 2173 } 2174 } 2175 `, 2176 }, 2177 "in-place update - insertion": { 2178 Action: plans.Update, 2179 Mode: addrs.ManagedResourceMode, 2180 Before: cty.ObjectVal(map[string]cty.Value{ 2181 "id": cty.StringVal("i-02ae66f368e8518a9"), 2182 "ami": cty.StringVal("ami-BEFORE"), 2183 "root_block_device": cty.ListVal([]cty.Value{ 2184 cty.ObjectVal(map[string]cty.Value{ 2185 "volume_type": cty.StringVal("gp2"), 2186 "new_field": cty.NullVal(cty.String), 2187 }), 2188 }), 2189 }), 2190 After: cty.ObjectVal(map[string]cty.Value{ 2191 "id": cty.StringVal("i-02ae66f368e8518a9"), 2192 "ami": cty.StringVal("ami-AFTER"), 2193 "root_block_device": cty.ListVal([]cty.Value{ 2194 cty.ObjectVal(map[string]cty.Value{ 2195 "volume_type": cty.StringVal("gp2"), 2196 "new_field": cty.StringVal("new_value"), 2197 }), 2198 }), 2199 }), 2200 RequiredReplace: cty.NewPathSet(), 2201 Tainted: false, 2202 Schema: &configschema.Block{ 2203 Attributes: map[string]*configschema.Attribute{ 2204 "id": {Type: cty.String, Optional: true, Computed: true}, 2205 "ami": {Type: cty.String, Optional: true}, 2206 }, 2207 BlockTypes: map[string]*configschema.NestedBlock{ 2208 "root_block_device": { 2209 Block: configschema.Block{ 2210 Attributes: map[string]*configschema.Attribute{ 2211 "volume_type": { 2212 Type: cty.String, 2213 Optional: true, 2214 Computed: true, 2215 }, 2216 "new_field": { 2217 Type: cty.String, 2218 Optional: true, 2219 Computed: true, 2220 }, 2221 }, 2222 }, 2223 Nesting: configschema.NestingList, 2224 }, 2225 }, 2226 }, 2227 ExpectedOutput: ` # test_instance.example will be updated in-place 2228 ~ resource "test_instance" "example" { 2229 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2230 id = "i-02ae66f368e8518a9" 2231 2232 ~ root_block_device { 2233 + new_field = "new_value" 2234 volume_type = "gp2" 2235 } 2236 } 2237 `, 2238 }, 2239 "force-new update (inside block)": { 2240 Action: plans.DeleteThenCreate, 2241 Mode: addrs.ManagedResourceMode, 2242 Before: cty.ObjectVal(map[string]cty.Value{ 2243 "id": cty.StringVal("i-02ae66f368e8518a9"), 2244 "ami": cty.StringVal("ami-BEFORE"), 2245 "root_block_device": cty.ListVal([]cty.Value{ 2246 cty.ObjectVal(map[string]cty.Value{ 2247 "volume_type": cty.StringVal("gp2"), 2248 }), 2249 }), 2250 }), 2251 After: cty.ObjectVal(map[string]cty.Value{ 2252 "id": cty.StringVal("i-02ae66f368e8518a9"), 2253 "ami": cty.StringVal("ami-AFTER"), 2254 "root_block_device": cty.ListVal([]cty.Value{ 2255 cty.ObjectVal(map[string]cty.Value{ 2256 "volume_type": cty.StringVal("different"), 2257 }), 2258 }), 2259 }), 2260 RequiredReplace: cty.NewPathSet(cty.Path{ 2261 cty.GetAttrStep{Name: "root_block_device"}, 2262 cty.IndexStep{Key: cty.NumberIntVal(0)}, 2263 cty.GetAttrStep{Name: "volume_type"}, 2264 }), 2265 Tainted: false, 2266 Schema: &configschema.Block{ 2267 Attributes: map[string]*configschema.Attribute{ 2268 "id": {Type: cty.String, Optional: true, Computed: true}, 2269 "ami": {Type: cty.String, Optional: true}, 2270 }, 2271 BlockTypes: map[string]*configschema.NestedBlock{ 2272 "root_block_device": { 2273 Block: configschema.Block{ 2274 Attributes: map[string]*configschema.Attribute{ 2275 "volume_type": { 2276 Type: cty.String, 2277 Optional: true, 2278 Computed: true, 2279 }, 2280 }, 2281 }, 2282 Nesting: configschema.NestingList, 2283 }, 2284 }, 2285 }, 2286 ExpectedOutput: ` # test_instance.example must be replaced 2287 -/+ resource "test_instance" "example" { 2288 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2289 id = "i-02ae66f368e8518a9" 2290 2291 ~ root_block_device { 2292 ~ volume_type = "gp2" -> "different" # forces replacement 2293 } 2294 } 2295 `, 2296 }, 2297 "force-new update (whole block)": { 2298 Action: plans.DeleteThenCreate, 2299 Mode: addrs.ManagedResourceMode, 2300 Before: cty.ObjectVal(map[string]cty.Value{ 2301 "id": cty.StringVal("i-02ae66f368e8518a9"), 2302 "ami": cty.StringVal("ami-BEFORE"), 2303 "root_block_device": cty.ListVal([]cty.Value{ 2304 cty.ObjectVal(map[string]cty.Value{ 2305 "volume_type": cty.StringVal("gp2"), 2306 }), 2307 }), 2308 }), 2309 After: cty.ObjectVal(map[string]cty.Value{ 2310 "id": cty.StringVal("i-02ae66f368e8518a9"), 2311 "ami": cty.StringVal("ami-AFTER"), 2312 "root_block_device": cty.ListVal([]cty.Value{ 2313 cty.ObjectVal(map[string]cty.Value{ 2314 "volume_type": cty.StringVal("different"), 2315 }), 2316 }), 2317 }), 2318 RequiredReplace: cty.NewPathSet(cty.Path{ 2319 cty.GetAttrStep{Name: "root_block_device"}, 2320 }), 2321 Tainted: false, 2322 Schema: &configschema.Block{ 2323 Attributes: map[string]*configschema.Attribute{ 2324 "id": {Type: cty.String, Optional: true, Computed: true}, 2325 "ami": {Type: cty.String, Optional: true}, 2326 }, 2327 BlockTypes: map[string]*configschema.NestedBlock{ 2328 "root_block_device": { 2329 Block: configschema.Block{ 2330 Attributes: map[string]*configschema.Attribute{ 2331 "volume_type": { 2332 Type: cty.String, 2333 Optional: true, 2334 Computed: true, 2335 }, 2336 }, 2337 }, 2338 Nesting: configschema.NestingList, 2339 }, 2340 }, 2341 }, 2342 ExpectedOutput: ` # test_instance.example must be replaced 2343 -/+ resource "test_instance" "example" { 2344 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2345 id = "i-02ae66f368e8518a9" 2346 2347 ~ root_block_device { # forces replacement 2348 ~ volume_type = "gp2" -> "different" 2349 } 2350 } 2351 `, 2352 }, 2353 "in-place update - deletion": { 2354 Action: plans.Update, 2355 Mode: addrs.ManagedResourceMode, 2356 Before: cty.ObjectVal(map[string]cty.Value{ 2357 "id": cty.StringVal("i-02ae66f368e8518a9"), 2358 "ami": cty.StringVal("ami-BEFORE"), 2359 "root_block_device": cty.ListVal([]cty.Value{ 2360 cty.ObjectVal(map[string]cty.Value{ 2361 "volume_type": cty.StringVal("gp2"), 2362 "new_field": cty.StringVal("new_value"), 2363 }), 2364 }), 2365 }), 2366 After: cty.ObjectVal(map[string]cty.Value{ 2367 "id": cty.StringVal("i-02ae66f368e8518a9"), 2368 "ami": cty.StringVal("ami-AFTER"), 2369 "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2370 "volume_type": cty.String, 2371 "new_field": cty.String, 2372 })), 2373 }), 2374 RequiredReplace: cty.NewPathSet(), 2375 Tainted: false, 2376 Schema: &configschema.Block{ 2377 Attributes: map[string]*configschema.Attribute{ 2378 "id": {Type: cty.String, Optional: true, Computed: true}, 2379 "ami": {Type: cty.String, Optional: true}, 2380 }, 2381 BlockTypes: map[string]*configschema.NestedBlock{ 2382 "root_block_device": { 2383 Block: configschema.Block{ 2384 Attributes: map[string]*configschema.Attribute{ 2385 "volume_type": { 2386 Type: cty.String, 2387 Optional: true, 2388 Computed: true, 2389 }, 2390 "new_field": { 2391 Type: cty.String, 2392 Optional: true, 2393 Computed: true, 2394 }, 2395 }, 2396 }, 2397 Nesting: configschema.NestingList, 2398 }, 2399 }, 2400 }, 2401 ExpectedOutput: ` # test_instance.example will be updated in-place 2402 ~ resource "test_instance" "example" { 2403 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2404 id = "i-02ae66f368e8518a9" 2405 2406 - root_block_device { 2407 - new_field = "new_value" -> null 2408 - volume_type = "gp2" -> null 2409 } 2410 } 2411 `, 2412 }, 2413 "with dynamically-typed attribute": { 2414 Action: plans.Update, 2415 Mode: addrs.ManagedResourceMode, 2416 Before: cty.ObjectVal(map[string]cty.Value{ 2417 "block": cty.EmptyTupleVal, 2418 }), 2419 After: cty.ObjectVal(map[string]cty.Value{ 2420 "block": cty.TupleVal([]cty.Value{ 2421 cty.ObjectVal(map[string]cty.Value{ 2422 "attr": cty.StringVal("foo"), 2423 }), 2424 cty.ObjectVal(map[string]cty.Value{ 2425 "attr": cty.True, 2426 }), 2427 }), 2428 }), 2429 RequiredReplace: cty.NewPathSet(), 2430 Tainted: false, 2431 Schema: &configschema.Block{ 2432 BlockTypes: map[string]*configschema.NestedBlock{ 2433 "block": { 2434 Block: configschema.Block{ 2435 Attributes: map[string]*configschema.Attribute{ 2436 "attr": {Type: cty.DynamicPseudoType, Optional: true}, 2437 }, 2438 }, 2439 Nesting: configschema.NestingList, 2440 }, 2441 }, 2442 }, 2443 ExpectedOutput: ` # test_instance.example will be updated in-place 2444 ~ resource "test_instance" "example" { 2445 + block { 2446 + attr = "foo" 2447 } 2448 + block { 2449 + attr = true 2450 } 2451 } 2452 `, 2453 }, 2454 } 2455 runTestCases(t, testCases) 2456 } 2457 2458 func TestResourceChange_nestedSet(t *testing.T) { 2459 testCases := map[string]testCase{ 2460 "in-place update - creation": { 2461 Action: plans.Update, 2462 Mode: addrs.ManagedResourceMode, 2463 Before: cty.ObjectVal(map[string]cty.Value{ 2464 "id": cty.StringVal("i-02ae66f368e8518a9"), 2465 "ami": cty.StringVal("ami-BEFORE"), 2466 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 2467 "volume_type": cty.String, 2468 })), 2469 }), 2470 After: cty.ObjectVal(map[string]cty.Value{ 2471 "id": cty.StringVal("i-02ae66f368e8518a9"), 2472 "ami": cty.StringVal("ami-AFTER"), 2473 "root_block_device": cty.SetVal([]cty.Value{ 2474 cty.ObjectVal(map[string]cty.Value{ 2475 "volume_type": cty.StringVal("gp2"), 2476 }), 2477 }), 2478 }), 2479 RequiredReplace: cty.NewPathSet(), 2480 Tainted: false, 2481 Schema: &configschema.Block{ 2482 Attributes: map[string]*configschema.Attribute{ 2483 "id": {Type: cty.String, Optional: true, Computed: true}, 2484 "ami": {Type: cty.String, Optional: true}, 2485 }, 2486 BlockTypes: map[string]*configschema.NestedBlock{ 2487 "root_block_device": { 2488 Block: configschema.Block{ 2489 Attributes: map[string]*configschema.Attribute{ 2490 "volume_type": { 2491 Type: cty.String, 2492 Optional: true, 2493 Computed: true, 2494 }, 2495 }, 2496 }, 2497 Nesting: configschema.NestingSet, 2498 }, 2499 }, 2500 }, 2501 ExpectedOutput: ` # test_instance.example will be updated in-place 2502 ~ resource "test_instance" "example" { 2503 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2504 id = "i-02ae66f368e8518a9" 2505 2506 + root_block_device { 2507 + volume_type = "gp2" 2508 } 2509 } 2510 `, 2511 }, 2512 "in-place update - insertion": { 2513 Action: plans.Update, 2514 Mode: addrs.ManagedResourceMode, 2515 Before: cty.ObjectVal(map[string]cty.Value{ 2516 "id": cty.StringVal("i-02ae66f368e8518a9"), 2517 "ami": cty.StringVal("ami-BEFORE"), 2518 "root_block_device": cty.SetVal([]cty.Value{ 2519 cty.ObjectVal(map[string]cty.Value{ 2520 "volume_type": cty.StringVal("gp2"), 2521 "new_field": cty.NullVal(cty.String), 2522 }), 2523 }), 2524 }), 2525 After: cty.ObjectVal(map[string]cty.Value{ 2526 "id": cty.StringVal("i-02ae66f368e8518a9"), 2527 "ami": cty.StringVal("ami-AFTER"), 2528 "root_block_device": cty.SetVal([]cty.Value{ 2529 cty.ObjectVal(map[string]cty.Value{ 2530 "volume_type": cty.StringVal("gp2"), 2531 "new_field": cty.StringVal("new_value"), 2532 }), 2533 }), 2534 }), 2535 RequiredReplace: cty.NewPathSet(), 2536 Tainted: false, 2537 Schema: &configschema.Block{ 2538 Attributes: map[string]*configschema.Attribute{ 2539 "id": {Type: cty.String, Optional: true, Computed: true}, 2540 "ami": {Type: cty.String, Optional: true}, 2541 }, 2542 BlockTypes: map[string]*configschema.NestedBlock{ 2543 "root_block_device": { 2544 Block: configschema.Block{ 2545 Attributes: map[string]*configschema.Attribute{ 2546 "volume_type": { 2547 Type: cty.String, 2548 Optional: true, 2549 Computed: true, 2550 }, 2551 "new_field": { 2552 Type: cty.String, 2553 Optional: true, 2554 Computed: true, 2555 }, 2556 }, 2557 }, 2558 Nesting: configschema.NestingSet, 2559 }, 2560 }, 2561 }, 2562 ExpectedOutput: ` # test_instance.example will be updated in-place 2563 ~ resource "test_instance" "example" { 2564 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2565 id = "i-02ae66f368e8518a9" 2566 2567 + root_block_device { 2568 + new_field = "new_value" 2569 + volume_type = "gp2" 2570 } 2571 - root_block_device { 2572 - volume_type = "gp2" -> null 2573 } 2574 } 2575 `, 2576 }, 2577 "force-new update (whole block)": { 2578 Action: plans.DeleteThenCreate, 2579 Mode: addrs.ManagedResourceMode, 2580 Before: cty.ObjectVal(map[string]cty.Value{ 2581 "id": cty.StringVal("i-02ae66f368e8518a9"), 2582 "ami": cty.StringVal("ami-BEFORE"), 2583 "root_block_device": cty.SetVal([]cty.Value{ 2584 cty.ObjectVal(map[string]cty.Value{ 2585 "volume_type": cty.StringVal("gp2"), 2586 }), 2587 }), 2588 }), 2589 After: cty.ObjectVal(map[string]cty.Value{ 2590 "id": cty.StringVal("i-02ae66f368e8518a9"), 2591 "ami": cty.StringVal("ami-AFTER"), 2592 "root_block_device": cty.SetVal([]cty.Value{ 2593 cty.ObjectVal(map[string]cty.Value{ 2594 "volume_type": cty.StringVal("different"), 2595 }), 2596 }), 2597 }), 2598 RequiredReplace: cty.NewPathSet(cty.Path{ 2599 cty.GetAttrStep{Name: "root_block_device"}, 2600 }), 2601 Tainted: false, 2602 Schema: &configschema.Block{ 2603 Attributes: map[string]*configschema.Attribute{ 2604 "id": {Type: cty.String, Optional: true, Computed: true}, 2605 "ami": {Type: cty.String, Optional: true}, 2606 }, 2607 BlockTypes: map[string]*configschema.NestedBlock{ 2608 "root_block_device": { 2609 Block: configschema.Block{ 2610 Attributes: map[string]*configschema.Attribute{ 2611 "volume_type": { 2612 Type: cty.String, 2613 Optional: true, 2614 Computed: true, 2615 }, 2616 }, 2617 }, 2618 Nesting: configschema.NestingSet, 2619 }, 2620 }, 2621 }, 2622 ExpectedOutput: ` # test_instance.example must be replaced 2623 -/+ resource "test_instance" "example" { 2624 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2625 id = "i-02ae66f368e8518a9" 2626 2627 + root_block_device { # forces replacement 2628 + volume_type = "different" 2629 } 2630 - root_block_device { # forces replacement 2631 - volume_type = "gp2" -> null 2632 } 2633 } 2634 `, 2635 }, 2636 "in-place update - deletion": { 2637 Action: plans.Update, 2638 Mode: addrs.ManagedResourceMode, 2639 Before: cty.ObjectVal(map[string]cty.Value{ 2640 "id": cty.StringVal("i-02ae66f368e8518a9"), 2641 "ami": cty.StringVal("ami-BEFORE"), 2642 "root_block_device": cty.SetVal([]cty.Value{ 2643 cty.ObjectVal(map[string]cty.Value{ 2644 "volume_type": cty.StringVal("gp2"), 2645 "new_field": cty.StringVal("new_value"), 2646 }), 2647 }), 2648 }), 2649 After: cty.ObjectVal(map[string]cty.Value{ 2650 "id": cty.StringVal("i-02ae66f368e8518a9"), 2651 "ami": cty.StringVal("ami-AFTER"), 2652 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 2653 "volume_type": cty.String, 2654 "new_field": cty.String, 2655 })), 2656 }), 2657 RequiredReplace: cty.NewPathSet(), 2658 Tainted: false, 2659 Schema: &configschema.Block{ 2660 Attributes: map[string]*configschema.Attribute{ 2661 "id": {Type: cty.String, Optional: true, Computed: true}, 2662 "ami": {Type: cty.String, Optional: true}, 2663 }, 2664 BlockTypes: map[string]*configschema.NestedBlock{ 2665 "root_block_device": { 2666 Block: configschema.Block{ 2667 Attributes: map[string]*configschema.Attribute{ 2668 "volume_type": { 2669 Type: cty.String, 2670 Optional: true, 2671 Computed: true, 2672 }, 2673 "new_field": { 2674 Type: cty.String, 2675 Optional: true, 2676 Computed: true, 2677 }, 2678 }, 2679 }, 2680 Nesting: configschema.NestingSet, 2681 }, 2682 }, 2683 }, 2684 ExpectedOutput: ` # test_instance.example will be updated in-place 2685 ~ resource "test_instance" "example" { 2686 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2687 id = "i-02ae66f368e8518a9" 2688 2689 - root_block_device { 2690 - new_field = "new_value" -> null 2691 - volume_type = "gp2" -> null 2692 } 2693 } 2694 `, 2695 }, 2696 } 2697 runTestCases(t, testCases) 2698 } 2699 2700 func TestResourceChange_nestedMap(t *testing.T) { 2701 testCases := map[string]testCase{ 2702 "in-place update - creation": { 2703 Action: plans.Update, 2704 Mode: addrs.ManagedResourceMode, 2705 Before: cty.ObjectVal(map[string]cty.Value{ 2706 "id": cty.StringVal("i-02ae66f368e8518a9"), 2707 "ami": cty.StringVal("ami-BEFORE"), 2708 "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 2709 "volume_type": cty.String, 2710 })), 2711 }), 2712 After: cty.ObjectVal(map[string]cty.Value{ 2713 "id": cty.StringVal("i-02ae66f368e8518a9"), 2714 "ami": cty.StringVal("ami-AFTER"), 2715 "root_block_device": cty.MapVal(map[string]cty.Value{ 2716 "a": cty.ObjectVal(map[string]cty.Value{ 2717 "volume_type": cty.StringVal("gp2"), 2718 }), 2719 }), 2720 }), 2721 RequiredReplace: cty.NewPathSet(), 2722 Tainted: false, 2723 Schema: &configschema.Block{ 2724 Attributes: map[string]*configschema.Attribute{ 2725 "id": {Type: cty.String, Optional: true, Computed: true}, 2726 "ami": {Type: cty.String, Optional: true}, 2727 }, 2728 BlockTypes: map[string]*configschema.NestedBlock{ 2729 "root_block_device": { 2730 Block: configschema.Block{ 2731 Attributes: map[string]*configschema.Attribute{ 2732 "volume_type": { 2733 Type: cty.String, 2734 Optional: true, 2735 Computed: true, 2736 }, 2737 }, 2738 }, 2739 Nesting: configschema.NestingMap, 2740 }, 2741 }, 2742 }, 2743 ExpectedOutput: ` # test_instance.example will be updated in-place 2744 ~ resource "test_instance" "example" { 2745 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2746 id = "i-02ae66f368e8518a9" 2747 2748 + root_block_device "a" { 2749 + volume_type = "gp2" 2750 } 2751 } 2752 `, 2753 }, 2754 "in-place update - change attr": { 2755 Action: plans.Update, 2756 Mode: addrs.ManagedResourceMode, 2757 Before: cty.ObjectVal(map[string]cty.Value{ 2758 "id": cty.StringVal("i-02ae66f368e8518a9"), 2759 "ami": cty.StringVal("ami-BEFORE"), 2760 "root_block_device": cty.MapVal(map[string]cty.Value{ 2761 "a": cty.ObjectVal(map[string]cty.Value{ 2762 "volume_type": cty.StringVal("gp2"), 2763 "new_field": cty.NullVal(cty.String), 2764 }), 2765 }), 2766 }), 2767 After: cty.ObjectVal(map[string]cty.Value{ 2768 "id": cty.StringVal("i-02ae66f368e8518a9"), 2769 "ami": cty.StringVal("ami-AFTER"), 2770 "root_block_device": cty.MapVal(map[string]cty.Value{ 2771 "a": cty.ObjectVal(map[string]cty.Value{ 2772 "volume_type": cty.StringVal("gp2"), 2773 "new_field": cty.StringVal("new_value"), 2774 }), 2775 }), 2776 }), 2777 RequiredReplace: cty.NewPathSet(), 2778 Tainted: false, 2779 Schema: &configschema.Block{ 2780 Attributes: map[string]*configschema.Attribute{ 2781 "id": {Type: cty.String, Optional: true, Computed: true}, 2782 "ami": {Type: cty.String, Optional: true}, 2783 }, 2784 BlockTypes: map[string]*configschema.NestedBlock{ 2785 "root_block_device": { 2786 Block: configschema.Block{ 2787 Attributes: map[string]*configschema.Attribute{ 2788 "volume_type": { 2789 Type: cty.String, 2790 Optional: true, 2791 Computed: true, 2792 }, 2793 "new_field": { 2794 Type: cty.String, 2795 Optional: true, 2796 Computed: true, 2797 }, 2798 }, 2799 }, 2800 Nesting: configschema.NestingMap, 2801 }, 2802 }, 2803 }, 2804 ExpectedOutput: ` # test_instance.example will be updated in-place 2805 ~ resource "test_instance" "example" { 2806 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2807 id = "i-02ae66f368e8518a9" 2808 2809 ~ root_block_device "a" { 2810 + new_field = "new_value" 2811 volume_type = "gp2" 2812 } 2813 } 2814 `, 2815 }, 2816 "in-place update - insertion": { 2817 Action: plans.Update, 2818 Mode: addrs.ManagedResourceMode, 2819 Before: cty.ObjectVal(map[string]cty.Value{ 2820 "id": cty.StringVal("i-02ae66f368e8518a9"), 2821 "ami": cty.StringVal("ami-BEFORE"), 2822 "root_block_device": cty.MapVal(map[string]cty.Value{ 2823 "a": cty.ObjectVal(map[string]cty.Value{ 2824 "volume_type": cty.StringVal("gp2"), 2825 "new_field": cty.NullVal(cty.String), 2826 }), 2827 }), 2828 }), 2829 After: cty.ObjectVal(map[string]cty.Value{ 2830 "id": cty.StringVal("i-02ae66f368e8518a9"), 2831 "ami": cty.StringVal("ami-AFTER"), 2832 "root_block_device": cty.MapVal(map[string]cty.Value{ 2833 "a": cty.ObjectVal(map[string]cty.Value{ 2834 "volume_type": cty.StringVal("gp2"), 2835 "new_field": cty.NullVal(cty.String), 2836 }), 2837 "b": cty.ObjectVal(map[string]cty.Value{ 2838 "volume_type": cty.StringVal("gp2"), 2839 "new_field": cty.StringVal("new_value"), 2840 }), 2841 }), 2842 }), 2843 RequiredReplace: cty.NewPathSet(), 2844 Tainted: false, 2845 Schema: &configschema.Block{ 2846 Attributes: map[string]*configschema.Attribute{ 2847 "id": {Type: cty.String, Optional: true, Computed: true}, 2848 "ami": {Type: cty.String, Optional: true}, 2849 }, 2850 BlockTypes: map[string]*configschema.NestedBlock{ 2851 "root_block_device": { 2852 Block: configschema.Block{ 2853 Attributes: map[string]*configschema.Attribute{ 2854 "volume_type": { 2855 Type: cty.String, 2856 Optional: true, 2857 Computed: true, 2858 }, 2859 "new_field": { 2860 Type: cty.String, 2861 Optional: true, 2862 Computed: true, 2863 }, 2864 }, 2865 }, 2866 Nesting: configschema.NestingMap, 2867 }, 2868 }, 2869 }, 2870 ExpectedOutput: ` # test_instance.example will be updated in-place 2871 ~ resource "test_instance" "example" { 2872 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2873 id = "i-02ae66f368e8518a9" 2874 2875 root_block_device "a" { 2876 volume_type = "gp2" 2877 } 2878 + root_block_device "b" { 2879 + new_field = "new_value" 2880 + volume_type = "gp2" 2881 } 2882 } 2883 `, 2884 }, 2885 "force-new update (whole block)": { 2886 Action: plans.DeleteThenCreate, 2887 Mode: addrs.ManagedResourceMode, 2888 Before: cty.ObjectVal(map[string]cty.Value{ 2889 "id": cty.StringVal("i-02ae66f368e8518a9"), 2890 "ami": cty.StringVal("ami-BEFORE"), 2891 "root_block_device": cty.MapVal(map[string]cty.Value{ 2892 "a": cty.ObjectVal(map[string]cty.Value{ 2893 "volume_type": cty.StringVal("gp2"), 2894 }), 2895 "b": cty.ObjectVal(map[string]cty.Value{ 2896 "volume_type": cty.StringVal("standard"), 2897 }), 2898 }), 2899 }), 2900 After: cty.ObjectVal(map[string]cty.Value{ 2901 "id": cty.StringVal("i-02ae66f368e8518a9"), 2902 "ami": cty.StringVal("ami-AFTER"), 2903 "root_block_device": cty.MapVal(map[string]cty.Value{ 2904 "a": cty.ObjectVal(map[string]cty.Value{ 2905 "volume_type": cty.StringVal("different"), 2906 }), 2907 "b": cty.ObjectVal(map[string]cty.Value{ 2908 "volume_type": cty.StringVal("standard"), 2909 }), 2910 }), 2911 }), 2912 RequiredReplace: cty.NewPathSet(cty.Path{ 2913 cty.GetAttrStep{Name: "root_block_device"}, 2914 cty.IndexStep{Key: cty.StringVal("a")}, 2915 }), 2916 Tainted: false, 2917 Schema: &configschema.Block{ 2918 Attributes: map[string]*configschema.Attribute{ 2919 "id": {Type: cty.String, Optional: true, Computed: true}, 2920 "ami": {Type: cty.String, Optional: true}, 2921 }, 2922 BlockTypes: map[string]*configschema.NestedBlock{ 2923 "root_block_device": { 2924 Block: configschema.Block{ 2925 Attributes: map[string]*configschema.Attribute{ 2926 "volume_type": { 2927 Type: cty.String, 2928 Optional: true, 2929 Computed: true, 2930 }, 2931 }, 2932 }, 2933 Nesting: configschema.NestingMap, 2934 }, 2935 }, 2936 }, 2937 ExpectedOutput: ` # test_instance.example must be replaced 2938 -/+ resource "test_instance" "example" { 2939 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2940 id = "i-02ae66f368e8518a9" 2941 2942 ~ root_block_device "a" { # forces replacement 2943 ~ volume_type = "gp2" -> "different" 2944 } 2945 root_block_device "b" { 2946 volume_type = "standard" 2947 } 2948 } 2949 `, 2950 }, 2951 "in-place update - deletion": { 2952 Action: plans.Update, 2953 Mode: addrs.ManagedResourceMode, 2954 Before: cty.ObjectVal(map[string]cty.Value{ 2955 "id": cty.StringVal("i-02ae66f368e8518a9"), 2956 "ami": cty.StringVal("ami-BEFORE"), 2957 "root_block_device": cty.MapVal(map[string]cty.Value{ 2958 "a": cty.ObjectVal(map[string]cty.Value{ 2959 "volume_type": cty.StringVal("gp2"), 2960 "new_field": cty.StringVal("new_value"), 2961 }), 2962 }), 2963 }), 2964 After: cty.ObjectVal(map[string]cty.Value{ 2965 "id": cty.StringVal("i-02ae66f368e8518a9"), 2966 "ami": cty.StringVal("ami-AFTER"), 2967 "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 2968 "volume_type": cty.String, 2969 "new_field": cty.String, 2970 })), 2971 }), 2972 RequiredReplace: cty.NewPathSet(), 2973 Tainted: false, 2974 Schema: &configschema.Block{ 2975 Attributes: map[string]*configschema.Attribute{ 2976 "id": {Type: cty.String, Optional: true, Computed: true}, 2977 "ami": {Type: cty.String, Optional: true}, 2978 }, 2979 BlockTypes: map[string]*configschema.NestedBlock{ 2980 "root_block_device": { 2981 Block: configschema.Block{ 2982 Attributes: map[string]*configschema.Attribute{ 2983 "volume_type": { 2984 Type: cty.String, 2985 Optional: true, 2986 Computed: true, 2987 }, 2988 "new_field": { 2989 Type: cty.String, 2990 Optional: true, 2991 Computed: true, 2992 }, 2993 }, 2994 }, 2995 Nesting: configschema.NestingMap, 2996 }, 2997 }, 2998 }, 2999 ExpectedOutput: ` # test_instance.example will be updated in-place 3000 ~ resource "test_instance" "example" { 3001 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3002 id = "i-02ae66f368e8518a9" 3003 3004 - root_block_device "a" { 3005 - new_field = "new_value" -> null 3006 - volume_type = "gp2" -> null 3007 } 3008 } 3009 `, 3010 }, 3011 "in-place sequence update - deletion": { 3012 Action: plans.Update, 3013 Mode: addrs.ManagedResourceMode, 3014 Before: cty.ObjectVal(map[string]cty.Value{ 3015 "list": cty.ListVal([]cty.Value{ 3016 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("x")}), 3017 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}), 3018 }), 3019 }), 3020 After: cty.ObjectVal(map[string]cty.Value{ 3021 "list": cty.ListVal([]cty.Value{ 3022 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}), 3023 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("z")}), 3024 }), 3025 }), 3026 RequiredReplace: cty.NewPathSet(), 3027 Tainted: false, 3028 Schema: &configschema.Block{ 3029 BlockTypes: map[string]*configschema.NestedBlock{ 3030 "list": { 3031 Block: configschema.Block{ 3032 Attributes: map[string]*configschema.Attribute{ 3033 "attr": { 3034 Type: cty.String, 3035 Required: true, 3036 }, 3037 }, 3038 }, 3039 Nesting: configschema.NestingList, 3040 }, 3041 }, 3042 }, 3043 ExpectedOutput: ` # test_instance.example will be updated in-place 3044 ~ resource "test_instance" "example" { 3045 ~ list { 3046 ~ attr = "x" -> "y" 3047 } 3048 ~ list { 3049 ~ attr = "y" -> "z" 3050 } 3051 } 3052 `, 3053 }, 3054 } 3055 runTestCases(t, testCases) 3056 } 3057 3058 type testCase struct { 3059 Action plans.Action 3060 Mode addrs.ResourceMode 3061 Before cty.Value 3062 After cty.Value 3063 Schema *configschema.Block 3064 RequiredReplace cty.PathSet 3065 Tainted bool 3066 ExpectedOutput string 3067 } 3068 3069 func runTestCases(t *testing.T, testCases map[string]testCase) { 3070 color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} 3071 3072 for name, tc := range testCases { 3073 t.Run(name, func(t *testing.T) { 3074 ty := tc.Schema.ImpliedType() 3075 3076 beforeVal := tc.Before 3077 switch { // Some fixups to make the test cases a little easier to write 3078 case beforeVal.IsNull(): 3079 beforeVal = cty.NullVal(ty) // allow mistyped nulls 3080 case !beforeVal.IsKnown(): 3081 beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns 3082 } 3083 before, err := plans.NewDynamicValue(beforeVal, ty) 3084 if err != nil { 3085 t.Fatal(err) 3086 } 3087 3088 afterVal := tc.After 3089 switch { // Some fixups to make the test cases a little easier to write 3090 case afterVal.IsNull(): 3091 afterVal = cty.NullVal(ty) // allow mistyped nulls 3092 case !afterVal.IsKnown(): 3093 afterVal = cty.UnknownVal(ty) // allow mistyped unknowns 3094 } 3095 after, err := plans.NewDynamicValue(afterVal, ty) 3096 if err != nil { 3097 t.Fatal(err) 3098 } 3099 3100 change := &plans.ResourceInstanceChangeSrc{ 3101 Addr: addrs.Resource{ 3102 Mode: tc.Mode, 3103 Type: "test_instance", 3104 Name: "example", 3105 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 3106 ProviderAddr: addrs.ProviderConfig{Type: "test"}.Absolute(addrs.RootModuleInstance), 3107 ChangeSrc: plans.ChangeSrc{ 3108 Action: tc.Action, 3109 Before: before, 3110 After: after, 3111 }, 3112 RequiredReplace: tc.RequiredReplace, 3113 } 3114 3115 output := ResourceChange(change, tc.Tainted, tc.Schema, color) 3116 if output != tc.ExpectedOutput { 3117 t.Fatalf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.ExpectedOutput) 3118 } 3119 }) 3120 } 3121 }