1package format 2 3import ( 4 "fmt" 5 "testing" 6 7 "github.com/google/go-cmp/cmp" 8 "github.com/hashicorp/terraform/internal/addrs" 9 "github.com/hashicorp/terraform/internal/configs/configschema" 10 "github.com/hashicorp/terraform/internal/plans" 11 "github.com/hashicorp/terraform/internal/states" 12 "github.com/mitchellh/colorstring" 13 "github.com/zclconf/go-cty/cty" 14) 15 16func TestResourceChange_primitiveTypes(t *testing.T) { 17 testCases := map[string]testCase{ 18 "creation": { 19 Action: plans.Create, 20 Mode: addrs.ManagedResourceMode, 21 Before: cty.NullVal(cty.EmptyObject), 22 After: cty.ObjectVal(map[string]cty.Value{ 23 "id": cty.UnknownVal(cty.String), 24 }), 25 Schema: &configschema.Block{ 26 Attributes: map[string]*configschema.Attribute{ 27 "id": {Type: cty.String, Computed: true}, 28 }, 29 }, 30 RequiredReplace: cty.NewPathSet(), 31 ExpectedOutput: ` # test_instance.example will be created 32 + resource "test_instance" "example" { 33 + id = (known after apply) 34 } 35`, 36 }, 37 "creation (null string)": { 38 Action: plans.Create, 39 Mode: addrs.ManagedResourceMode, 40 Before: cty.NullVal(cty.EmptyObject), 41 After: cty.ObjectVal(map[string]cty.Value{ 42 "string": cty.StringVal("null"), 43 }), 44 Schema: &configschema.Block{ 45 Attributes: map[string]*configschema.Attribute{ 46 "string": {Type: cty.String, Optional: true}, 47 }, 48 }, 49 RequiredReplace: cty.NewPathSet(), 50 ExpectedOutput: ` # test_instance.example will be created 51 + resource "test_instance" "example" { 52 + string = "null" 53 } 54`, 55 }, 56 "creation (null string with extra whitespace)": { 57 Action: plans.Create, 58 Mode: addrs.ManagedResourceMode, 59 Before: cty.NullVal(cty.EmptyObject), 60 After: cty.ObjectVal(map[string]cty.Value{ 61 "string": cty.StringVal("null "), 62 }), 63 Schema: &configschema.Block{ 64 Attributes: map[string]*configschema.Attribute{ 65 "string": {Type: cty.String, Optional: true}, 66 }, 67 }, 68 RequiredReplace: cty.NewPathSet(), 69 ExpectedOutput: ` # test_instance.example will be created 70 + resource "test_instance" "example" { 71 + string = "null " 72 } 73`, 74 }, 75 "deletion": { 76 Action: plans.Delete, 77 Mode: addrs.ManagedResourceMode, 78 Before: cty.ObjectVal(map[string]cty.Value{ 79 "id": cty.StringVal("i-02ae66f368e8518a9"), 80 }), 81 After: cty.NullVal(cty.EmptyObject), 82 Schema: &configschema.Block{ 83 Attributes: map[string]*configschema.Attribute{ 84 "id": {Type: cty.String, Computed: true}, 85 }, 86 }, 87 RequiredReplace: cty.NewPathSet(), 88 ExpectedOutput: ` # test_instance.example will be destroyed 89 - resource "test_instance" "example" { 90 - id = "i-02ae66f368e8518a9" -> null 91 } 92`, 93 }, 94 "deletion of deposed object": { 95 Action: plans.Delete, 96 Mode: addrs.ManagedResourceMode, 97 DeposedKey: states.DeposedKey("byebye"), 98 Before: cty.ObjectVal(map[string]cty.Value{ 99 "id": cty.StringVal("i-02ae66f368e8518a9"), 100 }), 101 After: cty.NullVal(cty.EmptyObject), 102 Schema: &configschema.Block{ 103 Attributes: map[string]*configschema.Attribute{ 104 "id": {Type: cty.String, Computed: true}, 105 }, 106 }, 107 RequiredReplace: cty.NewPathSet(), 108 ExpectedOutput: ` # test_instance.example (deposed object byebye) will be destroyed 109 # (left over from a partially-failed replacement of this instance) 110 - resource "test_instance" "example" { 111 - id = "i-02ae66f368e8518a9" -> null 112 } 113`, 114 }, 115 "deletion (empty string)": { 116 Action: plans.Delete, 117 Mode: addrs.ManagedResourceMode, 118 Before: cty.ObjectVal(map[string]cty.Value{ 119 "id": cty.StringVal("i-02ae66f368e8518a9"), 120 "intentionally_long": cty.StringVal(""), 121 }), 122 After: cty.NullVal(cty.EmptyObject), 123 Schema: &configschema.Block{ 124 Attributes: map[string]*configschema.Attribute{ 125 "id": {Type: cty.String, Computed: true}, 126 "intentionally_long": {Type: cty.String, Optional: true}, 127 }, 128 }, 129 RequiredReplace: cty.NewPathSet(), 130 ExpectedOutput: ` # test_instance.example will be destroyed 131 - resource "test_instance" "example" { 132 - id = "i-02ae66f368e8518a9" -> null 133 } 134`, 135 }, 136 "string in-place update": { 137 Action: plans.Update, 138 Mode: addrs.ManagedResourceMode, 139 Before: cty.ObjectVal(map[string]cty.Value{ 140 "id": cty.StringVal("i-02ae66f368e8518a9"), 141 "ami": cty.StringVal("ami-BEFORE"), 142 }), 143 After: cty.ObjectVal(map[string]cty.Value{ 144 "id": cty.StringVal("i-02ae66f368e8518a9"), 145 "ami": cty.StringVal("ami-AFTER"), 146 }), 147 Schema: &configschema.Block{ 148 Attributes: map[string]*configschema.Attribute{ 149 "id": {Type: cty.String, Optional: true, Computed: true}, 150 "ami": {Type: cty.String, Optional: true}, 151 }, 152 }, 153 RequiredReplace: cty.NewPathSet(), 154 ExpectedOutput: ` # test_instance.example will be updated in-place 155 ~ resource "test_instance" "example" { 156 ~ ami = "ami-BEFORE" -> "ami-AFTER" 157 id = "i-02ae66f368e8518a9" 158 } 159`, 160 }, 161 "string force-new update": { 162 Action: plans.DeleteThenCreate, 163 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 164 Mode: addrs.ManagedResourceMode, 165 Before: cty.ObjectVal(map[string]cty.Value{ 166 "id": cty.StringVal("i-02ae66f368e8518a9"), 167 "ami": cty.StringVal("ami-BEFORE"), 168 }), 169 After: cty.ObjectVal(map[string]cty.Value{ 170 "id": cty.StringVal("i-02ae66f368e8518a9"), 171 "ami": cty.StringVal("ami-AFTER"), 172 }), 173 Schema: &configschema.Block{ 174 Attributes: map[string]*configschema.Attribute{ 175 "id": {Type: cty.String, Optional: true, Computed: true}, 176 "ami": {Type: cty.String, Optional: true}, 177 }, 178 }, 179 RequiredReplace: cty.NewPathSet(cty.Path{ 180 cty.GetAttrStep{Name: "ami"}, 181 }), 182 ExpectedOutput: ` # test_instance.example must be replaced 183-/+ resource "test_instance" "example" { 184 ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement 185 id = "i-02ae66f368e8518a9" 186 } 187`, 188 }, 189 "string in-place update (null values)": { 190 Action: plans.Update, 191 Mode: addrs.ManagedResourceMode, 192 Before: cty.ObjectVal(map[string]cty.Value{ 193 "id": cty.StringVal("i-02ae66f368e8518a9"), 194 "ami": cty.StringVal("ami-BEFORE"), 195 "unchanged": cty.NullVal(cty.String), 196 }), 197 After: cty.ObjectVal(map[string]cty.Value{ 198 "id": cty.StringVal("i-02ae66f368e8518a9"), 199 "ami": cty.StringVal("ami-AFTER"), 200 "unchanged": cty.NullVal(cty.String), 201 }), 202 Schema: &configschema.Block{ 203 Attributes: map[string]*configschema.Attribute{ 204 "id": {Type: cty.String, Optional: true, Computed: true}, 205 "ami": {Type: cty.String, Optional: true}, 206 "unchanged": {Type: cty.String, Optional: true}, 207 }, 208 }, 209 RequiredReplace: cty.NewPathSet(), 210 ExpectedOutput: ` # test_instance.example will be updated in-place 211 ~ resource "test_instance" "example" { 212 ~ ami = "ami-BEFORE" -> "ami-AFTER" 213 id = "i-02ae66f368e8518a9" 214 } 215`, 216 }, 217 "in-place update of multi-line string field": { 218 Action: plans.Update, 219 Mode: addrs.ManagedResourceMode, 220 Before: cty.ObjectVal(map[string]cty.Value{ 221 "id": cty.StringVal("i-02ae66f368e8518a9"), 222 "more_lines": cty.StringVal(`original 223long 224multi-line 225string 226field 227`), 228 }), 229 After: cty.ObjectVal(map[string]cty.Value{ 230 "id": cty.UnknownVal(cty.String), 231 "more_lines": cty.StringVal(`original 232extremely long 233multi-line 234string 235field 236`), 237 }), 238 Schema: &configschema.Block{ 239 Attributes: map[string]*configschema.Attribute{ 240 "id": {Type: cty.String, Optional: true, Computed: true}, 241 "more_lines": {Type: cty.String, Optional: true}, 242 }, 243 }, 244 RequiredReplace: cty.NewPathSet(), 245 ExpectedOutput: ` # test_instance.example will be updated in-place 246 ~ resource "test_instance" "example" { 247 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 248 ~ more_lines = <<-EOT 249 original 250 - long 251 + extremely long 252 multi-line 253 string 254 field 255 EOT 256 } 257`, 258 }, 259 "addition of multi-line string field": { 260 Action: plans.Update, 261 Mode: addrs.ManagedResourceMode, 262 Before: cty.ObjectVal(map[string]cty.Value{ 263 "id": cty.StringVal("i-02ae66f368e8518a9"), 264 "more_lines": cty.NullVal(cty.String), 265 }), 266 After: cty.ObjectVal(map[string]cty.Value{ 267 "id": cty.UnknownVal(cty.String), 268 "more_lines": cty.StringVal(`original 269new line 270`), 271 }), 272 Schema: &configschema.Block{ 273 Attributes: map[string]*configschema.Attribute{ 274 "id": {Type: cty.String, Optional: true, Computed: true}, 275 "more_lines": {Type: cty.String, Optional: true}, 276 }, 277 }, 278 RequiredReplace: cty.NewPathSet(), 279 ExpectedOutput: ` # test_instance.example will be updated in-place 280 ~ resource "test_instance" "example" { 281 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 282 + more_lines = <<-EOT 283 original 284 new line 285 EOT 286 } 287`, 288 }, 289 "force-new update of multi-line string field": { 290 Action: plans.DeleteThenCreate, 291 Mode: addrs.ManagedResourceMode, 292 Before: cty.ObjectVal(map[string]cty.Value{ 293 "id": cty.StringVal("i-02ae66f368e8518a9"), 294 "more_lines": cty.StringVal(`original 295`), 296 }), 297 After: cty.ObjectVal(map[string]cty.Value{ 298 "id": cty.UnknownVal(cty.String), 299 "more_lines": cty.StringVal(`original 300new line 301`), 302 }), 303 Schema: &configschema.Block{ 304 Attributes: map[string]*configschema.Attribute{ 305 "id": {Type: cty.String, Optional: true, Computed: true}, 306 "more_lines": {Type: cty.String, Optional: true}, 307 }, 308 }, 309 RequiredReplace: cty.NewPathSet(cty.Path{ 310 cty.GetAttrStep{Name: "more_lines"}, 311 }), 312 ExpectedOutput: ` # test_instance.example must be replaced 313-/+ resource "test_instance" "example" { 314 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 315 ~ more_lines = <<-EOT # forces replacement 316 original 317 + new line 318 EOT 319 } 320`, 321 }, 322 323 // Sensitive 324 325 "creation with sensitive field": { 326 Action: plans.Create, 327 Mode: addrs.ManagedResourceMode, 328 Before: cty.NullVal(cty.EmptyObject), 329 After: cty.ObjectVal(map[string]cty.Value{ 330 "id": cty.UnknownVal(cty.String), 331 "password": cty.StringVal("top-secret"), 332 "conn_info": cty.ObjectVal(map[string]cty.Value{ 333 "user": cty.StringVal("not-secret"), 334 "password": cty.StringVal("top-secret"), 335 }), 336 }), 337 Schema: &configschema.Block{ 338 Attributes: map[string]*configschema.Attribute{ 339 "id": {Type: cty.String, Computed: true}, 340 "password": {Type: cty.String, Optional: true, Sensitive: true}, 341 "conn_info": { 342 NestedType: &configschema.Object{ 343 Nesting: configschema.NestingSingle, 344 Attributes: map[string]*configschema.Attribute{ 345 "user": {Type: cty.String, Optional: true}, 346 "password": {Type: cty.String, Optional: true, Sensitive: true}, 347 }, 348 }, 349 }, 350 }, 351 }, 352 RequiredReplace: cty.NewPathSet(), 353 ExpectedOutput: ` # test_instance.example will be created 354 + resource "test_instance" "example" { 355 + conn_info = { 356 + password = (sensitive value) 357 + user = "not-secret" 358 } 359 + id = (known after apply) 360 + password = (sensitive value) 361 } 362`, 363 }, 364 "update with equal sensitive field": { 365 Action: plans.Update, 366 Mode: addrs.ManagedResourceMode, 367 Before: cty.ObjectVal(map[string]cty.Value{ 368 "id": cty.StringVal("blah"), 369 "str": cty.StringVal("before"), 370 "password": cty.StringVal("top-secret"), 371 }), 372 After: cty.ObjectVal(map[string]cty.Value{ 373 "id": cty.UnknownVal(cty.String), 374 "str": cty.StringVal("after"), 375 "password": cty.StringVal("top-secret"), 376 }), 377 Schema: &configschema.Block{ 378 Attributes: map[string]*configschema.Attribute{ 379 "id": {Type: cty.String, Computed: true}, 380 "str": {Type: cty.String, Optional: true}, 381 "password": {Type: cty.String, Optional: true, Sensitive: true}, 382 }, 383 }, 384 RequiredReplace: cty.NewPathSet(), 385 ExpectedOutput: ` # test_instance.example will be updated in-place 386 ~ resource "test_instance" "example" { 387 ~ id = "blah" -> (known after apply) 388 ~ str = "before" -> "after" 389 # (1 unchanged attribute hidden) 390 } 391`, 392 }, 393 394 // tainted objects 395 "replace tainted resource": { 396 Action: plans.DeleteThenCreate, 397 ActionReason: plans.ResourceInstanceReplaceBecauseTainted, 398 Mode: addrs.ManagedResourceMode, 399 Before: cty.ObjectVal(map[string]cty.Value{ 400 "id": cty.StringVal("i-02ae66f368e8518a9"), 401 "ami": cty.StringVal("ami-BEFORE"), 402 }), 403 After: cty.ObjectVal(map[string]cty.Value{ 404 "id": cty.UnknownVal(cty.String), 405 "ami": cty.StringVal("ami-AFTER"), 406 }), 407 Schema: &configschema.Block{ 408 Attributes: map[string]*configschema.Attribute{ 409 "id": {Type: cty.String, Optional: true, Computed: true}, 410 "ami": {Type: cty.String, Optional: true}, 411 }, 412 }, 413 RequiredReplace: cty.NewPathSet(cty.Path{ 414 cty.GetAttrStep{Name: "ami"}, 415 }), 416 ExpectedOutput: ` # test_instance.example is tainted, so must be replaced 417-/+ resource "test_instance" "example" { 418 ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement 419 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 420 } 421`, 422 }, 423 "force replacement with empty before value": { 424 Action: plans.DeleteThenCreate, 425 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 426 Mode: addrs.ManagedResourceMode, 427 Before: cty.ObjectVal(map[string]cty.Value{ 428 "name": cty.StringVal("name"), 429 "forced": cty.NullVal(cty.String), 430 }), 431 After: cty.ObjectVal(map[string]cty.Value{ 432 "name": cty.StringVal("name"), 433 "forced": cty.StringVal("example"), 434 }), 435 Schema: &configschema.Block{ 436 Attributes: map[string]*configschema.Attribute{ 437 "name": {Type: cty.String, Optional: true}, 438 "forced": {Type: cty.String, Optional: true}, 439 }, 440 }, 441 RequiredReplace: cty.NewPathSet(cty.Path{ 442 cty.GetAttrStep{Name: "forced"}, 443 }), 444 ExpectedOutput: ` # test_instance.example must be replaced 445-/+ resource "test_instance" "example" { 446 + forced = "example" # forces replacement 447 name = "name" 448 } 449`, 450 }, 451 "force replacement with empty before value legacy": { 452 Action: plans.DeleteThenCreate, 453 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 454 Mode: addrs.ManagedResourceMode, 455 Before: cty.ObjectVal(map[string]cty.Value{ 456 "name": cty.StringVal("name"), 457 "forced": cty.StringVal(""), 458 }), 459 After: cty.ObjectVal(map[string]cty.Value{ 460 "name": cty.StringVal("name"), 461 "forced": cty.StringVal("example"), 462 }), 463 Schema: &configschema.Block{ 464 Attributes: map[string]*configschema.Attribute{ 465 "name": {Type: cty.String, Optional: true}, 466 "forced": {Type: cty.String, Optional: true}, 467 }, 468 }, 469 RequiredReplace: cty.NewPathSet(cty.Path{ 470 cty.GetAttrStep{Name: "forced"}, 471 }), 472 ExpectedOutput: ` # test_instance.example must be replaced 473-/+ resource "test_instance" "example" { 474 + forced = "example" # forces replacement 475 name = "name" 476 } 477`, 478 }, 479 "show all identifying attributes even if unchanged": { 480 Action: plans.Update, 481 Mode: addrs.ManagedResourceMode, 482 Before: cty.ObjectVal(map[string]cty.Value{ 483 "id": cty.StringVal("i-02ae66f368e8518a9"), 484 "ami": cty.StringVal("ami-BEFORE"), 485 "bar": cty.StringVal("bar"), 486 "foo": cty.StringVal("foo"), 487 "name": cty.StringVal("alice"), 488 "tags": cty.MapVal(map[string]cty.Value{ 489 "name": cty.StringVal("bob"), 490 }), 491 }), 492 After: cty.ObjectVal(map[string]cty.Value{ 493 "id": cty.StringVal("i-02ae66f368e8518a9"), 494 "ami": cty.StringVal("ami-AFTER"), 495 "bar": cty.StringVal("bar"), 496 "foo": cty.StringVal("foo"), 497 "name": cty.StringVal("alice"), 498 "tags": cty.MapVal(map[string]cty.Value{ 499 "name": cty.StringVal("bob"), 500 }), 501 }), 502 Schema: &configschema.Block{ 503 Attributes: map[string]*configschema.Attribute{ 504 "id": {Type: cty.String, Optional: true, Computed: true}, 505 "ami": {Type: cty.String, Optional: true}, 506 "bar": {Type: cty.String, Optional: true}, 507 "foo": {Type: cty.String, Optional: true}, 508 "name": {Type: cty.String, Optional: true}, 509 "tags": {Type: cty.Map(cty.String), Optional: true}, 510 }, 511 }, 512 RequiredReplace: cty.NewPathSet(), 513 ExpectedOutput: ` # test_instance.example will be updated in-place 514 ~ resource "test_instance" "example" { 515 ~ ami = "ami-BEFORE" -> "ami-AFTER" 516 id = "i-02ae66f368e8518a9" 517 name = "alice" 518 tags = { 519 "name" = "bob" 520 } 521 # (2 unchanged attributes hidden) 522 } 523`, 524 }, 525 } 526 527 runTestCases(t, testCases) 528} 529 530func TestResourceChange_JSON(t *testing.T) { 531 testCases := map[string]testCase{ 532 "creation": { 533 Action: plans.Create, 534 Mode: addrs.ManagedResourceMode, 535 Before: cty.NullVal(cty.EmptyObject), 536 After: cty.ObjectVal(map[string]cty.Value{ 537 "id": cty.UnknownVal(cty.String), 538 "json_field": cty.StringVal(`{ 539 "str": "value", 540 "list":["a","b", 234, true], 541 "obj": {"key": "val"} 542 }`), 543 }), 544 Schema: &configschema.Block{ 545 Attributes: map[string]*configschema.Attribute{ 546 "id": {Type: cty.String, Optional: true, Computed: true}, 547 "json_field": {Type: cty.String, Optional: true}, 548 }, 549 }, 550 RequiredReplace: cty.NewPathSet(), 551 ExpectedOutput: ` # test_instance.example will be created 552 + resource "test_instance" "example" { 553 + id = (known after apply) 554 + json_field = jsonencode( 555 { 556 + list = [ 557 + "a", 558 + "b", 559 + 234, 560 + true, 561 ] 562 + obj = { 563 + key = "val" 564 } 565 + str = "value" 566 } 567 ) 568 } 569`, 570 }, 571 "in-place update of object": { 572 Action: plans.Update, 573 Mode: addrs.ManagedResourceMode, 574 Before: cty.ObjectVal(map[string]cty.Value{ 575 "id": cty.StringVal("i-02ae66f368e8518a9"), 576 "json_field": cty.StringVal(`{"aaa": "value","ccc": 5}`), 577 }), 578 After: cty.ObjectVal(map[string]cty.Value{ 579 "id": cty.UnknownVal(cty.String), 580 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`), 581 }), 582 Schema: &configschema.Block{ 583 Attributes: map[string]*configschema.Attribute{ 584 "id": {Type: cty.String, Optional: true, Computed: true}, 585 "json_field": {Type: cty.String, Optional: true}, 586 }, 587 }, 588 RequiredReplace: cty.NewPathSet(), 589 ExpectedOutput: ` # test_instance.example will be updated in-place 590 ~ resource "test_instance" "example" { 591 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 592 ~ json_field = jsonencode( 593 ~ { 594 + bbb = "new_value" 595 - ccc = 5 -> null 596 # (1 unchanged element hidden) 597 } 598 ) 599 } 600`, 601 }, 602 "in-place update (from empty tuple)": { 603 Action: plans.Update, 604 Mode: addrs.ManagedResourceMode, 605 Before: cty.ObjectVal(map[string]cty.Value{ 606 "id": cty.StringVal("i-02ae66f368e8518a9"), 607 "json_field": cty.StringVal(`{"aaa": []}`), 608 }), 609 After: cty.ObjectVal(map[string]cty.Value{ 610 "id": cty.UnknownVal(cty.String), 611 "json_field": cty.StringVal(`{"aaa": ["value"]}`), 612 }), 613 Schema: &configschema.Block{ 614 Attributes: map[string]*configschema.Attribute{ 615 "id": {Type: cty.String, Optional: true, Computed: true}, 616 "json_field": {Type: cty.String, Optional: true}, 617 }, 618 }, 619 RequiredReplace: cty.NewPathSet(), 620 ExpectedOutput: ` # test_instance.example will be updated in-place 621 ~ resource "test_instance" "example" { 622 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 623 ~ json_field = jsonencode( 624 ~ { 625 ~ aaa = [ 626 + "value", 627 ] 628 } 629 ) 630 } 631`, 632 }, 633 "in-place update (to empty tuple)": { 634 Action: plans.Update, 635 Mode: addrs.ManagedResourceMode, 636 Before: cty.ObjectVal(map[string]cty.Value{ 637 "id": cty.StringVal("i-02ae66f368e8518a9"), 638 "json_field": cty.StringVal(`{"aaa": ["value"]}`), 639 }), 640 After: cty.ObjectVal(map[string]cty.Value{ 641 "id": cty.UnknownVal(cty.String), 642 "json_field": cty.StringVal(`{"aaa": []}`), 643 }), 644 Schema: &configschema.Block{ 645 Attributes: map[string]*configschema.Attribute{ 646 "id": {Type: cty.String, Optional: true, Computed: true}, 647 "json_field": {Type: cty.String, Optional: true}, 648 }, 649 }, 650 RequiredReplace: cty.NewPathSet(), 651 ExpectedOutput: ` # test_instance.example will be updated in-place 652 ~ resource "test_instance" "example" { 653 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 654 ~ json_field = jsonencode( 655 ~ { 656 ~ aaa = [ 657 - "value", 658 ] 659 } 660 ) 661 } 662`, 663 }, 664 "in-place update (tuple of different types)": { 665 Action: plans.Update, 666 Mode: addrs.ManagedResourceMode, 667 Before: cty.ObjectVal(map[string]cty.Value{ 668 "id": cty.StringVal("i-02ae66f368e8518a9"), 669 "json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`), 670 }), 671 After: cty.ObjectVal(map[string]cty.Value{ 672 "id": cty.UnknownVal(cty.String), 673 "json_field": cty.StringVal(`{"aaa": [42, {"foo":"baz"}, "value"]}`), 674 }), 675 Schema: &configschema.Block{ 676 Attributes: map[string]*configschema.Attribute{ 677 "id": {Type: cty.String, Optional: true, Computed: true}, 678 "json_field": {Type: cty.String, Optional: true}, 679 }, 680 }, 681 RequiredReplace: cty.NewPathSet(), 682 ExpectedOutput: ` # test_instance.example will be updated in-place 683 ~ resource "test_instance" "example" { 684 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 685 ~ json_field = jsonencode( 686 ~ { 687 ~ aaa = [ 688 42, 689 ~ { 690 ~ foo = "bar" -> "baz" 691 }, 692 "value", 693 ] 694 } 695 ) 696 } 697`, 698 }, 699 "force-new update": { 700 Action: plans.DeleteThenCreate, 701 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 702 Mode: addrs.ManagedResourceMode, 703 Before: cty.ObjectVal(map[string]cty.Value{ 704 "id": cty.StringVal("i-02ae66f368e8518a9"), 705 "json_field": cty.StringVal(`{"aaa": "value"}`), 706 }), 707 After: cty.ObjectVal(map[string]cty.Value{ 708 "id": cty.UnknownVal(cty.String), 709 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`), 710 }), 711 Schema: &configschema.Block{ 712 Attributes: map[string]*configschema.Attribute{ 713 "id": {Type: cty.String, Optional: true, Computed: true}, 714 "json_field": {Type: cty.String, Optional: true}, 715 }, 716 }, 717 RequiredReplace: cty.NewPathSet(cty.Path{ 718 cty.GetAttrStep{Name: "json_field"}, 719 }), 720 ExpectedOutput: ` # test_instance.example must be replaced 721-/+ resource "test_instance" "example" { 722 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 723 ~ json_field = jsonencode( 724 ~ { 725 + bbb = "new_value" 726 # (1 unchanged element hidden) 727 } # forces replacement 728 ) 729 } 730`, 731 }, 732 "in-place update (whitespace change)": { 733 Action: plans.Update, 734 Mode: addrs.ManagedResourceMode, 735 Before: cty.ObjectVal(map[string]cty.Value{ 736 "id": cty.StringVal("i-02ae66f368e8518a9"), 737 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`), 738 }), 739 After: cty.ObjectVal(map[string]cty.Value{ 740 "id": cty.UnknownVal(cty.String), 741 "json_field": cty.StringVal(`{"aaa":"value", 742 "bbb":"another"}`), 743 }), 744 Schema: &configschema.Block{ 745 Attributes: map[string]*configschema.Attribute{ 746 "id": {Type: cty.String, Optional: true, Computed: true}, 747 "json_field": {Type: cty.String, Optional: true}, 748 }, 749 }, 750 RequiredReplace: cty.NewPathSet(), 751 ExpectedOutput: ` # test_instance.example will be updated in-place 752 ~ resource "test_instance" "example" { 753 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 754 ~ json_field = jsonencode( # whitespace changes 755 { 756 aaa = "value" 757 bbb = "another" 758 } 759 ) 760 } 761`, 762 }, 763 "force-new update (whitespace change)": { 764 Action: plans.DeleteThenCreate, 765 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 766 Mode: addrs.ManagedResourceMode, 767 Before: cty.ObjectVal(map[string]cty.Value{ 768 "id": cty.StringVal("i-02ae66f368e8518a9"), 769 "json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`), 770 }), 771 After: cty.ObjectVal(map[string]cty.Value{ 772 "id": cty.UnknownVal(cty.String), 773 "json_field": cty.StringVal(`{"aaa":"value", 774 "bbb":"another"}`), 775 }), 776 Schema: &configschema.Block{ 777 Attributes: map[string]*configschema.Attribute{ 778 "id": {Type: cty.String, Optional: true, Computed: true}, 779 "json_field": {Type: cty.String, Optional: true}, 780 }, 781 }, 782 RequiredReplace: cty.NewPathSet(cty.Path{ 783 cty.GetAttrStep{Name: "json_field"}, 784 }), 785 ExpectedOutput: ` # test_instance.example must be replaced 786-/+ resource "test_instance" "example" { 787 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 788 ~ json_field = jsonencode( # whitespace changes force replacement 789 { 790 aaa = "value" 791 bbb = "another" 792 } 793 ) 794 } 795`, 796 }, 797 "creation (empty)": { 798 Action: plans.Create, 799 Mode: addrs.ManagedResourceMode, 800 Before: cty.NullVal(cty.EmptyObject), 801 After: cty.ObjectVal(map[string]cty.Value{ 802 "id": cty.UnknownVal(cty.String), 803 "json_field": cty.StringVal(`{}`), 804 }), 805 Schema: &configschema.Block{ 806 Attributes: map[string]*configschema.Attribute{ 807 "id": {Type: cty.String, Optional: true, Computed: true}, 808 "json_field": {Type: cty.String, Optional: true}, 809 }, 810 }, 811 RequiredReplace: cty.NewPathSet(), 812 ExpectedOutput: ` # test_instance.example will be created 813 + resource "test_instance" "example" { 814 + id = (known after apply) 815 + json_field = jsonencode({}) 816 } 817`, 818 }, 819 "JSON list item removal": { 820 Action: plans.Update, 821 Mode: addrs.ManagedResourceMode, 822 Before: cty.ObjectVal(map[string]cty.Value{ 823 "id": cty.StringVal("i-02ae66f368e8518a9"), 824 "json_field": cty.StringVal(`["first","second","third"]`), 825 }), 826 After: cty.ObjectVal(map[string]cty.Value{ 827 "id": cty.UnknownVal(cty.String), 828 "json_field": cty.StringVal(`["first","second"]`), 829 }), 830 Schema: &configschema.Block{ 831 Attributes: map[string]*configschema.Attribute{ 832 "id": {Type: cty.String, Optional: true, Computed: true}, 833 "json_field": {Type: cty.String, Optional: true}, 834 }, 835 }, 836 RequiredReplace: cty.NewPathSet(), 837 ExpectedOutput: ` # test_instance.example will be updated in-place 838 ~ resource "test_instance" "example" { 839 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 840 ~ json_field = jsonencode( 841 ~ [ 842 # (1 unchanged element hidden) 843 "second", 844 - "third", 845 ] 846 ) 847 } 848`, 849 }, 850 "JSON list item addition": { 851 Action: plans.Update, 852 Mode: addrs.ManagedResourceMode, 853 Before: cty.ObjectVal(map[string]cty.Value{ 854 "id": cty.StringVal("i-02ae66f368e8518a9"), 855 "json_field": cty.StringVal(`["first","second"]`), 856 }), 857 After: cty.ObjectVal(map[string]cty.Value{ 858 "id": cty.UnknownVal(cty.String), 859 "json_field": cty.StringVal(`["first","second","third"]`), 860 }), 861 Schema: &configschema.Block{ 862 Attributes: map[string]*configschema.Attribute{ 863 "id": {Type: cty.String, Optional: true, Computed: true}, 864 "json_field": {Type: cty.String, Optional: true}, 865 }, 866 }, 867 RequiredReplace: cty.NewPathSet(), 868 ExpectedOutput: ` # test_instance.example will be updated in-place 869 ~ resource "test_instance" "example" { 870 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 871 ~ json_field = jsonencode( 872 ~ [ 873 # (1 unchanged element hidden) 874 "second", 875 + "third", 876 ] 877 ) 878 } 879`, 880 }, 881 "JSON list object addition": { 882 Action: plans.Update, 883 Mode: addrs.ManagedResourceMode, 884 Before: cty.ObjectVal(map[string]cty.Value{ 885 "id": cty.StringVal("i-02ae66f368e8518a9"), 886 "json_field": cty.StringVal(`{"first":"111"}`), 887 }), 888 After: cty.ObjectVal(map[string]cty.Value{ 889 "id": cty.UnknownVal(cty.String), 890 "json_field": cty.StringVal(`{"first":"111","second":"222"}`), 891 }), 892 Schema: &configschema.Block{ 893 Attributes: map[string]*configschema.Attribute{ 894 "id": {Type: cty.String, Optional: true, Computed: true}, 895 "json_field": {Type: cty.String, Optional: true}, 896 }, 897 }, 898 RequiredReplace: cty.NewPathSet(), 899 ExpectedOutput: ` # test_instance.example will be updated in-place 900 ~ resource "test_instance" "example" { 901 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 902 ~ json_field = jsonencode( 903 ~ { 904 + second = "222" 905 # (1 unchanged element hidden) 906 } 907 ) 908 } 909`, 910 }, 911 "JSON object with nested list": { 912 Action: plans.Update, 913 Mode: addrs.ManagedResourceMode, 914 Before: cty.ObjectVal(map[string]cty.Value{ 915 "id": cty.StringVal("i-02ae66f368e8518a9"), 916 "json_field": cty.StringVal(`{ 917 "Statement": ["first"] 918 }`), 919 }), 920 After: cty.ObjectVal(map[string]cty.Value{ 921 "id": cty.UnknownVal(cty.String), 922 "json_field": cty.StringVal(`{ 923 "Statement": ["first", "second"] 924 }`), 925 }), 926 Schema: &configschema.Block{ 927 Attributes: map[string]*configschema.Attribute{ 928 "id": {Type: cty.String, Optional: true, Computed: true}, 929 "json_field": {Type: cty.String, Optional: true}, 930 }, 931 }, 932 RequiredReplace: cty.NewPathSet(), 933 ExpectedOutput: ` # test_instance.example will be updated in-place 934 ~ resource "test_instance" "example" { 935 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 936 ~ json_field = jsonencode( 937 ~ { 938 ~ Statement = [ 939 "first", 940 + "second", 941 ] 942 } 943 ) 944 } 945`, 946 }, 947 "JSON list of objects - adding item": { 948 Action: plans.Update, 949 Mode: addrs.ManagedResourceMode, 950 Before: cty.ObjectVal(map[string]cty.Value{ 951 "id": cty.StringVal("i-02ae66f368e8518a9"), 952 "json_field": cty.StringVal(`[{"one": "111"}]`), 953 }), 954 After: cty.ObjectVal(map[string]cty.Value{ 955 "id": cty.UnknownVal(cty.String), 956 "json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`), 957 }), 958 Schema: &configschema.Block{ 959 Attributes: map[string]*configschema.Attribute{ 960 "id": {Type: cty.String, Optional: true, Computed: true}, 961 "json_field": {Type: cty.String, Optional: true}, 962 }, 963 }, 964 RequiredReplace: cty.NewPathSet(), 965 ExpectedOutput: ` # test_instance.example will be updated in-place 966 ~ resource "test_instance" "example" { 967 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 968 ~ json_field = jsonencode( 969 ~ [ 970 { 971 one = "111" 972 }, 973 + { 974 + two = "222" 975 }, 976 ] 977 ) 978 } 979`, 980 }, 981 "JSON list of objects - removing item": { 982 Action: plans.Update, 983 Mode: addrs.ManagedResourceMode, 984 Before: cty.ObjectVal(map[string]cty.Value{ 985 "id": cty.StringVal("i-02ae66f368e8518a9"), 986 "json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}, {"three": "333"}]`), 987 }), 988 After: cty.ObjectVal(map[string]cty.Value{ 989 "id": cty.UnknownVal(cty.String), 990 "json_field": cty.StringVal(`[{"one": "111"}, {"three": "333"}]`), 991 }), 992 Schema: &configschema.Block{ 993 Attributes: map[string]*configschema.Attribute{ 994 "id": {Type: cty.String, Optional: true, Computed: true}, 995 "json_field": {Type: cty.String, Optional: true}, 996 }, 997 }, 998 RequiredReplace: cty.NewPathSet(), 999 ExpectedOutput: ` # test_instance.example will be updated in-place 1000 ~ resource "test_instance" "example" { 1001 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1002 ~ json_field = jsonencode( 1003 ~ [ 1004 { 1005 one = "111" 1006 }, 1007 - { 1008 - two = "222" 1009 }, 1010 { 1011 three = "333" 1012 }, 1013 ] 1014 ) 1015 } 1016`, 1017 }, 1018 "JSON object with list of objects": { 1019 Action: plans.Update, 1020 Mode: addrs.ManagedResourceMode, 1021 Before: cty.ObjectVal(map[string]cty.Value{ 1022 "id": cty.StringVal("i-02ae66f368e8518a9"), 1023 "json_field": cty.StringVal(`{"parent":[{"one": "111"}]}`), 1024 }), 1025 After: cty.ObjectVal(map[string]cty.Value{ 1026 "id": cty.UnknownVal(cty.String), 1027 "json_field": cty.StringVal(`{"parent":[{"one": "111"}, {"two": "222"}]}`), 1028 }), 1029 Schema: &configschema.Block{ 1030 Attributes: map[string]*configschema.Attribute{ 1031 "id": {Type: cty.String, Optional: true, Computed: true}, 1032 "json_field": {Type: cty.String, Optional: true}, 1033 }, 1034 }, 1035 RequiredReplace: cty.NewPathSet(), 1036 ExpectedOutput: ` # test_instance.example will be updated in-place 1037 ~ resource "test_instance" "example" { 1038 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1039 ~ json_field = jsonencode( 1040 ~ { 1041 ~ parent = [ 1042 { 1043 one = "111" 1044 }, 1045 + { 1046 + two = "222" 1047 }, 1048 ] 1049 } 1050 ) 1051 } 1052`, 1053 }, 1054 "JSON object double nested lists": { 1055 Action: plans.Update, 1056 Mode: addrs.ManagedResourceMode, 1057 Before: cty.ObjectVal(map[string]cty.Value{ 1058 "id": cty.StringVal("i-02ae66f368e8518a9"), 1059 "json_field": cty.StringVal(`{"parent":[{"another_list": ["111"]}]}`), 1060 }), 1061 After: cty.ObjectVal(map[string]cty.Value{ 1062 "id": cty.UnknownVal(cty.String), 1063 "json_field": cty.StringVal(`{"parent":[{"another_list": ["111", "222"]}]}`), 1064 }), 1065 Schema: &configschema.Block{ 1066 Attributes: map[string]*configschema.Attribute{ 1067 "id": {Type: cty.String, Optional: true, Computed: true}, 1068 "json_field": {Type: cty.String, Optional: true}, 1069 }, 1070 }, 1071 RequiredReplace: cty.NewPathSet(), 1072 ExpectedOutput: ` # test_instance.example will be updated in-place 1073 ~ resource "test_instance" "example" { 1074 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1075 ~ json_field = jsonencode( 1076 ~ { 1077 ~ parent = [ 1078 ~ { 1079 ~ another_list = [ 1080 "111", 1081 + "222", 1082 ] 1083 }, 1084 ] 1085 } 1086 ) 1087 } 1088`, 1089 }, 1090 "in-place update from object to tuple": { 1091 Action: plans.Update, 1092 Mode: addrs.ManagedResourceMode, 1093 Before: cty.ObjectVal(map[string]cty.Value{ 1094 "id": cty.StringVal("i-02ae66f368e8518a9"), 1095 "json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`), 1096 }), 1097 After: cty.ObjectVal(map[string]cty.Value{ 1098 "id": cty.UnknownVal(cty.String), 1099 "json_field": cty.StringVal(`["aaa", 42, "something"]`), 1100 }), 1101 Schema: &configschema.Block{ 1102 Attributes: map[string]*configschema.Attribute{ 1103 "id": {Type: cty.String, Optional: true, Computed: true}, 1104 "json_field": {Type: cty.String, Optional: true}, 1105 }, 1106 }, 1107 RequiredReplace: cty.NewPathSet(), 1108 ExpectedOutput: ` # test_instance.example will be updated in-place 1109 ~ resource "test_instance" "example" { 1110 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1111 ~ json_field = jsonencode( 1112 ~ { 1113 - aaa = [ 1114 - 42, 1115 - { 1116 - foo = "bar" 1117 }, 1118 - "value", 1119 ] 1120 } -> [ 1121 + "aaa", 1122 + 42, 1123 + "something", 1124 ] 1125 ) 1126 } 1127`, 1128 }, 1129 } 1130 runTestCases(t, testCases) 1131} 1132 1133func TestResourceChange_primitiveList(t *testing.T) { 1134 testCases := map[string]testCase{ 1135 "in-place update - creation": { 1136 Action: plans.Update, 1137 Mode: addrs.ManagedResourceMode, 1138 Before: cty.ObjectVal(map[string]cty.Value{ 1139 "id": cty.StringVal("i-02ae66f368e8518a9"), 1140 "ami": cty.StringVal("ami-STATIC"), 1141 "list_field": cty.NullVal(cty.List(cty.String)), 1142 }), 1143 After: cty.ObjectVal(map[string]cty.Value{ 1144 "id": cty.UnknownVal(cty.String), 1145 "ami": cty.StringVal("ami-STATIC"), 1146 "list_field": cty.ListVal([]cty.Value{ 1147 cty.StringVal("new-element"), 1148 }), 1149 }), 1150 Schema: &configschema.Block{ 1151 Attributes: map[string]*configschema.Attribute{ 1152 "id": {Type: cty.String, Optional: true, Computed: true}, 1153 "ami": {Type: cty.String, Optional: true}, 1154 "list_field": {Type: cty.List(cty.String), Optional: true}, 1155 }, 1156 }, 1157 RequiredReplace: cty.NewPathSet(), 1158 ExpectedOutput: ` # test_instance.example will be updated in-place 1159 ~ resource "test_instance" "example" { 1160 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1161 + list_field = [ 1162 + "new-element", 1163 ] 1164 # (1 unchanged attribute hidden) 1165 } 1166`, 1167 }, 1168 "in-place update - first addition": { 1169 Action: plans.Update, 1170 Mode: addrs.ManagedResourceMode, 1171 Before: cty.ObjectVal(map[string]cty.Value{ 1172 "id": cty.StringVal("i-02ae66f368e8518a9"), 1173 "ami": cty.StringVal("ami-STATIC"), 1174 "list_field": cty.ListValEmpty(cty.String), 1175 }), 1176 After: cty.ObjectVal(map[string]cty.Value{ 1177 "id": cty.UnknownVal(cty.String), 1178 "ami": cty.StringVal("ami-STATIC"), 1179 "list_field": cty.ListVal([]cty.Value{ 1180 cty.StringVal("new-element"), 1181 }), 1182 }), 1183 Schema: &configschema.Block{ 1184 Attributes: map[string]*configschema.Attribute{ 1185 "id": {Type: cty.String, Optional: true, Computed: true}, 1186 "ami": {Type: cty.String, Optional: true}, 1187 "list_field": {Type: cty.List(cty.String), Optional: true}, 1188 }, 1189 }, 1190 RequiredReplace: cty.NewPathSet(), 1191 ExpectedOutput: ` # test_instance.example will be updated in-place 1192 ~ resource "test_instance" "example" { 1193 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1194 ~ list_field = [ 1195 + "new-element", 1196 ] 1197 # (1 unchanged attribute hidden) 1198 } 1199`, 1200 }, 1201 "in-place update - insertion": { 1202 Action: plans.Update, 1203 Mode: addrs.ManagedResourceMode, 1204 Before: cty.ObjectVal(map[string]cty.Value{ 1205 "id": cty.StringVal("i-02ae66f368e8518a9"), 1206 "ami": cty.StringVal("ami-STATIC"), 1207 "list_field": cty.ListVal([]cty.Value{ 1208 cty.StringVal("aaaa"), 1209 cty.StringVal("bbbb"), 1210 cty.StringVal("dddd"), 1211 cty.StringVal("eeee"), 1212 cty.StringVal("ffff"), 1213 }), 1214 }), 1215 After: cty.ObjectVal(map[string]cty.Value{ 1216 "id": cty.UnknownVal(cty.String), 1217 "ami": cty.StringVal("ami-STATIC"), 1218 "list_field": cty.ListVal([]cty.Value{ 1219 cty.StringVal("aaaa"), 1220 cty.StringVal("bbbb"), 1221 cty.StringVal("cccc"), 1222 cty.StringVal("dddd"), 1223 cty.StringVal("eeee"), 1224 cty.StringVal("ffff"), 1225 }), 1226 }), 1227 Schema: &configschema.Block{ 1228 Attributes: map[string]*configschema.Attribute{ 1229 "id": {Type: cty.String, Optional: true, Computed: true}, 1230 "ami": {Type: cty.String, Optional: true}, 1231 "list_field": {Type: cty.List(cty.String), Optional: true}, 1232 }, 1233 }, 1234 RequiredReplace: cty.NewPathSet(), 1235 ExpectedOutput: ` # test_instance.example will be updated in-place 1236 ~ resource "test_instance" "example" { 1237 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1238 ~ list_field = [ 1239 # (1 unchanged element hidden) 1240 "bbbb", 1241 + "cccc", 1242 "dddd", 1243 # (2 unchanged elements hidden) 1244 ] 1245 # (1 unchanged attribute hidden) 1246 } 1247`, 1248 }, 1249 "force-new update - insertion": { 1250 Action: plans.DeleteThenCreate, 1251 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 1252 Mode: addrs.ManagedResourceMode, 1253 Before: cty.ObjectVal(map[string]cty.Value{ 1254 "id": cty.StringVal("i-02ae66f368e8518a9"), 1255 "ami": cty.StringVal("ami-STATIC"), 1256 "list_field": cty.ListVal([]cty.Value{ 1257 cty.StringVal("aaaa"), 1258 cty.StringVal("cccc"), 1259 }), 1260 }), 1261 After: cty.ObjectVal(map[string]cty.Value{ 1262 "id": cty.UnknownVal(cty.String), 1263 "ami": cty.StringVal("ami-STATIC"), 1264 "list_field": cty.ListVal([]cty.Value{ 1265 cty.StringVal("aaaa"), 1266 cty.StringVal("bbbb"), 1267 cty.StringVal("cccc"), 1268 }), 1269 }), 1270 Schema: &configschema.Block{ 1271 Attributes: map[string]*configschema.Attribute{ 1272 "id": {Type: cty.String, Optional: true, Computed: true}, 1273 "ami": {Type: cty.String, Optional: true}, 1274 "list_field": {Type: cty.List(cty.String), Optional: true}, 1275 }, 1276 }, 1277 RequiredReplace: cty.NewPathSet(cty.Path{ 1278 cty.GetAttrStep{Name: "list_field"}, 1279 }), 1280 ExpectedOutput: ` # test_instance.example must be replaced 1281-/+ resource "test_instance" "example" { 1282 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1283 ~ list_field = [ # forces replacement 1284 "aaaa", 1285 + "bbbb", 1286 "cccc", 1287 ] 1288 # (1 unchanged attribute hidden) 1289 } 1290`, 1291 }, 1292 "in-place update - deletion": { 1293 Action: plans.Update, 1294 Mode: addrs.ManagedResourceMode, 1295 Before: cty.ObjectVal(map[string]cty.Value{ 1296 "id": cty.StringVal("i-02ae66f368e8518a9"), 1297 "ami": cty.StringVal("ami-STATIC"), 1298 "list_field": cty.ListVal([]cty.Value{ 1299 cty.StringVal("aaaa"), 1300 cty.StringVal("bbbb"), 1301 cty.StringVal("cccc"), 1302 cty.StringVal("dddd"), 1303 cty.StringVal("eeee"), 1304 }), 1305 }), 1306 After: cty.ObjectVal(map[string]cty.Value{ 1307 "id": cty.UnknownVal(cty.String), 1308 "ami": cty.StringVal("ami-STATIC"), 1309 "list_field": cty.ListVal([]cty.Value{ 1310 cty.StringVal("bbbb"), 1311 cty.StringVal("dddd"), 1312 cty.StringVal("eeee"), 1313 }), 1314 }), 1315 Schema: &configschema.Block{ 1316 Attributes: map[string]*configschema.Attribute{ 1317 "id": {Type: cty.String, Optional: true, Computed: true}, 1318 "ami": {Type: cty.String, Optional: true}, 1319 "list_field": {Type: cty.List(cty.String), Optional: true}, 1320 }, 1321 }, 1322 RequiredReplace: cty.NewPathSet(), 1323 ExpectedOutput: ` # test_instance.example will be updated in-place 1324 ~ resource "test_instance" "example" { 1325 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1326 ~ list_field = [ 1327 - "aaaa", 1328 "bbbb", 1329 - "cccc", 1330 "dddd", 1331 # (1 unchanged element hidden) 1332 ] 1333 # (1 unchanged attribute hidden) 1334 } 1335`, 1336 }, 1337 "creation - empty list": { 1338 Action: plans.Create, 1339 Mode: addrs.ManagedResourceMode, 1340 Before: cty.NullVal(cty.EmptyObject), 1341 After: cty.ObjectVal(map[string]cty.Value{ 1342 "id": cty.UnknownVal(cty.String), 1343 "ami": cty.StringVal("ami-STATIC"), 1344 "list_field": cty.ListValEmpty(cty.String), 1345 }), 1346 Schema: &configschema.Block{ 1347 Attributes: map[string]*configschema.Attribute{ 1348 "id": {Type: cty.String, Optional: true, Computed: true}, 1349 "ami": {Type: cty.String, Optional: true}, 1350 "list_field": {Type: cty.List(cty.String), Optional: true}, 1351 }, 1352 }, 1353 RequiredReplace: cty.NewPathSet(), 1354 ExpectedOutput: ` # test_instance.example will be created 1355 + resource "test_instance" "example" { 1356 + ami = "ami-STATIC" 1357 + id = (known after apply) 1358 + list_field = [] 1359 } 1360`, 1361 }, 1362 "in-place update - full to empty": { 1363 Action: plans.Update, 1364 Mode: addrs.ManagedResourceMode, 1365 Before: cty.ObjectVal(map[string]cty.Value{ 1366 "id": cty.StringVal("i-02ae66f368e8518a9"), 1367 "ami": cty.StringVal("ami-STATIC"), 1368 "list_field": cty.ListVal([]cty.Value{ 1369 cty.StringVal("aaaa"), 1370 cty.StringVal("bbbb"), 1371 cty.StringVal("cccc"), 1372 }), 1373 }), 1374 After: cty.ObjectVal(map[string]cty.Value{ 1375 "id": cty.UnknownVal(cty.String), 1376 "ami": cty.StringVal("ami-STATIC"), 1377 "list_field": cty.ListValEmpty(cty.String), 1378 }), 1379 Schema: &configschema.Block{ 1380 Attributes: map[string]*configschema.Attribute{ 1381 "id": {Type: cty.String, Optional: true, Computed: true}, 1382 "ami": {Type: cty.String, Optional: true}, 1383 "list_field": {Type: cty.List(cty.String), Optional: true}, 1384 }, 1385 }, 1386 RequiredReplace: cty.NewPathSet(), 1387 ExpectedOutput: ` # test_instance.example will be updated in-place 1388 ~ resource "test_instance" "example" { 1389 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1390 ~ list_field = [ 1391 - "aaaa", 1392 - "bbbb", 1393 - "cccc", 1394 ] 1395 # (1 unchanged attribute hidden) 1396 } 1397`, 1398 }, 1399 "in-place update - null to empty": { 1400 Action: plans.Update, 1401 Mode: addrs.ManagedResourceMode, 1402 Before: cty.ObjectVal(map[string]cty.Value{ 1403 "id": cty.StringVal("i-02ae66f368e8518a9"), 1404 "ami": cty.StringVal("ami-STATIC"), 1405 "list_field": cty.NullVal(cty.List(cty.String)), 1406 }), 1407 After: cty.ObjectVal(map[string]cty.Value{ 1408 "id": cty.UnknownVal(cty.String), 1409 "ami": cty.StringVal("ami-STATIC"), 1410 "list_field": cty.ListValEmpty(cty.String), 1411 }), 1412 Schema: &configschema.Block{ 1413 Attributes: map[string]*configschema.Attribute{ 1414 "id": {Type: cty.String, Optional: true, Computed: true}, 1415 "ami": {Type: cty.String, Optional: true}, 1416 "list_field": {Type: cty.List(cty.String), Optional: true}, 1417 }, 1418 }, 1419 RequiredReplace: cty.NewPathSet(), 1420 ExpectedOutput: ` # test_instance.example will be updated in-place 1421 ~ resource "test_instance" "example" { 1422 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1423 + list_field = [] 1424 # (1 unchanged attribute hidden) 1425 } 1426`, 1427 }, 1428 "update to unknown element": { 1429 Action: plans.Update, 1430 Mode: addrs.ManagedResourceMode, 1431 Before: cty.ObjectVal(map[string]cty.Value{ 1432 "id": cty.StringVal("i-02ae66f368e8518a9"), 1433 "ami": cty.StringVal("ami-STATIC"), 1434 "list_field": cty.ListVal([]cty.Value{ 1435 cty.StringVal("aaaa"), 1436 cty.StringVal("bbbb"), 1437 cty.StringVal("cccc"), 1438 }), 1439 }), 1440 After: cty.ObjectVal(map[string]cty.Value{ 1441 "id": cty.UnknownVal(cty.String), 1442 "ami": cty.StringVal("ami-STATIC"), 1443 "list_field": cty.ListVal([]cty.Value{ 1444 cty.StringVal("aaaa"), 1445 cty.UnknownVal(cty.String), 1446 cty.StringVal("cccc"), 1447 }), 1448 }), 1449 Schema: &configschema.Block{ 1450 Attributes: map[string]*configschema.Attribute{ 1451 "id": {Type: cty.String, Optional: true, Computed: true}, 1452 "ami": {Type: cty.String, Optional: true}, 1453 "list_field": {Type: cty.List(cty.String), Optional: true}, 1454 }, 1455 }, 1456 RequiredReplace: cty.NewPathSet(), 1457 ExpectedOutput: ` # test_instance.example will be updated in-place 1458 ~ resource "test_instance" "example" { 1459 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1460 ~ list_field = [ 1461 "aaaa", 1462 - "bbbb", 1463 + (known after apply), 1464 "cccc", 1465 ] 1466 # (1 unchanged attribute hidden) 1467 } 1468`, 1469 }, 1470 "update - two new unknown elements": { 1471 Action: plans.Update, 1472 Mode: addrs.ManagedResourceMode, 1473 Before: cty.ObjectVal(map[string]cty.Value{ 1474 "id": cty.StringVal("i-02ae66f368e8518a9"), 1475 "ami": cty.StringVal("ami-STATIC"), 1476 "list_field": cty.ListVal([]cty.Value{ 1477 cty.StringVal("aaaa"), 1478 cty.StringVal("bbbb"), 1479 cty.StringVal("cccc"), 1480 cty.StringVal("dddd"), 1481 cty.StringVal("eeee"), 1482 }), 1483 }), 1484 After: cty.ObjectVal(map[string]cty.Value{ 1485 "id": cty.UnknownVal(cty.String), 1486 "ami": cty.StringVal("ami-STATIC"), 1487 "list_field": cty.ListVal([]cty.Value{ 1488 cty.StringVal("aaaa"), 1489 cty.UnknownVal(cty.String), 1490 cty.UnknownVal(cty.String), 1491 cty.StringVal("cccc"), 1492 cty.StringVal("dddd"), 1493 cty.StringVal("eeee"), 1494 }), 1495 }), 1496 Schema: &configschema.Block{ 1497 Attributes: map[string]*configschema.Attribute{ 1498 "id": {Type: cty.String, Optional: true, Computed: true}, 1499 "ami": {Type: cty.String, Optional: true}, 1500 "list_field": {Type: cty.List(cty.String), Optional: true}, 1501 }, 1502 }, 1503 RequiredReplace: cty.NewPathSet(), 1504 ExpectedOutput: ` # test_instance.example will be updated in-place 1505 ~ resource "test_instance" "example" { 1506 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1507 ~ list_field = [ 1508 "aaaa", 1509 - "bbbb", 1510 + (known after apply), 1511 + (known after apply), 1512 "cccc", 1513 # (2 unchanged elements hidden) 1514 ] 1515 # (1 unchanged attribute hidden) 1516 } 1517`, 1518 }, 1519 } 1520 runTestCases(t, testCases) 1521} 1522 1523func TestResourceChange_primitiveTuple(t *testing.T) { 1524 testCases := map[string]testCase{ 1525 "in-place update": { 1526 Action: plans.Update, 1527 Mode: addrs.ManagedResourceMode, 1528 Before: cty.ObjectVal(map[string]cty.Value{ 1529 "id": cty.StringVal("i-02ae66f368e8518a9"), 1530 "tuple_field": cty.TupleVal([]cty.Value{ 1531 cty.StringVal("aaaa"), 1532 cty.StringVal("bbbb"), 1533 cty.StringVal("dddd"), 1534 cty.StringVal("eeee"), 1535 cty.StringVal("ffff"), 1536 }), 1537 }), 1538 After: cty.ObjectVal(map[string]cty.Value{ 1539 "id": cty.StringVal("i-02ae66f368e8518a9"), 1540 "tuple_field": cty.TupleVal([]cty.Value{ 1541 cty.StringVal("aaaa"), 1542 cty.StringVal("bbbb"), 1543 cty.StringVal("cccc"), 1544 cty.StringVal("eeee"), 1545 cty.StringVal("ffff"), 1546 }), 1547 }), 1548 Schema: &configschema.Block{ 1549 Attributes: map[string]*configschema.Attribute{ 1550 "id": {Type: cty.String, Required: true}, 1551 "tuple_field": {Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.String, cty.String, cty.String}), Optional: true}, 1552 }, 1553 }, 1554 RequiredReplace: cty.NewPathSet(), 1555 ExpectedOutput: ` # test_instance.example will be updated in-place 1556 ~ resource "test_instance" "example" { 1557 id = "i-02ae66f368e8518a9" 1558 ~ tuple_field = [ 1559 # (1 unchanged element hidden) 1560 "bbbb", 1561 - "dddd", 1562 + "cccc", 1563 "eeee", 1564 # (1 unchanged element hidden) 1565 ] 1566 } 1567`, 1568 }, 1569 } 1570 runTestCases(t, testCases) 1571} 1572 1573func TestResourceChange_primitiveSet(t *testing.T) { 1574 testCases := map[string]testCase{ 1575 "in-place update - creation": { 1576 Action: plans.Update, 1577 Mode: addrs.ManagedResourceMode, 1578 Before: cty.ObjectVal(map[string]cty.Value{ 1579 "id": cty.StringVal("i-02ae66f368e8518a9"), 1580 "ami": cty.StringVal("ami-STATIC"), 1581 "set_field": cty.NullVal(cty.Set(cty.String)), 1582 }), 1583 After: cty.ObjectVal(map[string]cty.Value{ 1584 "id": cty.UnknownVal(cty.String), 1585 "ami": cty.StringVal("ami-STATIC"), 1586 "set_field": cty.SetVal([]cty.Value{ 1587 cty.StringVal("new-element"), 1588 }), 1589 }), 1590 Schema: &configschema.Block{ 1591 Attributes: map[string]*configschema.Attribute{ 1592 "id": {Type: cty.String, Optional: true, Computed: true}, 1593 "ami": {Type: cty.String, Optional: true}, 1594 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1595 }, 1596 }, 1597 RequiredReplace: cty.NewPathSet(), 1598 ExpectedOutput: ` # test_instance.example will be updated in-place 1599 ~ resource "test_instance" "example" { 1600 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1601 + set_field = [ 1602 + "new-element", 1603 ] 1604 # (1 unchanged attribute hidden) 1605 } 1606`, 1607 }, 1608 "in-place update - first insertion": { 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.SetValEmpty(cty.String), 1615 }), 1616 After: cty.ObjectVal(map[string]cty.Value{ 1617 "id": cty.UnknownVal(cty.String), 1618 "ami": cty.StringVal("ami-STATIC"), 1619 "set_field": cty.SetVal([]cty.Value{ 1620 cty.StringVal("new-element"), 1621 }), 1622 }), 1623 Schema: &configschema.Block{ 1624 Attributes: map[string]*configschema.Attribute{ 1625 "id": {Type: cty.String, Optional: true, Computed: true}, 1626 "ami": {Type: cty.String, Optional: true}, 1627 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1628 }, 1629 }, 1630 RequiredReplace: cty.NewPathSet(), 1631 ExpectedOutput: ` # test_instance.example will be updated in-place 1632 ~ resource "test_instance" "example" { 1633 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1634 ~ set_field = [ 1635 + "new-element", 1636 ] 1637 # (1 unchanged attribute hidden) 1638 } 1639`, 1640 }, 1641 "in-place update - insertion": { 1642 Action: plans.Update, 1643 Mode: addrs.ManagedResourceMode, 1644 Before: cty.ObjectVal(map[string]cty.Value{ 1645 "id": cty.StringVal("i-02ae66f368e8518a9"), 1646 "ami": cty.StringVal("ami-STATIC"), 1647 "set_field": cty.SetVal([]cty.Value{ 1648 cty.StringVal("aaaa"), 1649 cty.StringVal("cccc"), 1650 }), 1651 }), 1652 After: cty.ObjectVal(map[string]cty.Value{ 1653 "id": cty.UnknownVal(cty.String), 1654 "ami": cty.StringVal("ami-STATIC"), 1655 "set_field": cty.SetVal([]cty.Value{ 1656 cty.StringVal("aaaa"), 1657 cty.StringVal("bbbb"), 1658 cty.StringVal("cccc"), 1659 }), 1660 }), 1661 Schema: &configschema.Block{ 1662 Attributes: map[string]*configschema.Attribute{ 1663 "id": {Type: cty.String, Optional: true, Computed: true}, 1664 "ami": {Type: cty.String, Optional: true}, 1665 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1666 }, 1667 }, 1668 RequiredReplace: cty.NewPathSet(), 1669 ExpectedOutput: ` # test_instance.example will be updated in-place 1670 ~ resource "test_instance" "example" { 1671 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1672 ~ set_field = [ 1673 + "bbbb", 1674 # (2 unchanged elements hidden) 1675 ] 1676 # (1 unchanged attribute hidden) 1677 } 1678`, 1679 }, 1680 "force-new update - insertion": { 1681 Action: plans.DeleteThenCreate, 1682 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 1683 Mode: addrs.ManagedResourceMode, 1684 Before: cty.ObjectVal(map[string]cty.Value{ 1685 "id": cty.StringVal("i-02ae66f368e8518a9"), 1686 "ami": cty.StringVal("ami-STATIC"), 1687 "set_field": cty.SetVal([]cty.Value{ 1688 cty.StringVal("aaaa"), 1689 cty.StringVal("cccc"), 1690 }), 1691 }), 1692 After: cty.ObjectVal(map[string]cty.Value{ 1693 "id": cty.UnknownVal(cty.String), 1694 "ami": cty.StringVal("ami-STATIC"), 1695 "set_field": cty.SetVal([]cty.Value{ 1696 cty.StringVal("aaaa"), 1697 cty.StringVal("bbbb"), 1698 cty.StringVal("cccc"), 1699 }), 1700 }), 1701 Schema: &configschema.Block{ 1702 Attributes: map[string]*configschema.Attribute{ 1703 "id": {Type: cty.String, Optional: true, Computed: true}, 1704 "ami": {Type: cty.String, Optional: true}, 1705 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1706 }, 1707 }, 1708 RequiredReplace: cty.NewPathSet(cty.Path{ 1709 cty.GetAttrStep{Name: "set_field"}, 1710 }), 1711 ExpectedOutput: ` # test_instance.example must be replaced 1712-/+ resource "test_instance" "example" { 1713 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1714 ~ set_field = [ # forces replacement 1715 + "bbbb", 1716 # (2 unchanged elements hidden) 1717 ] 1718 # (1 unchanged attribute hidden) 1719 } 1720`, 1721 }, 1722 "in-place update - deletion": { 1723 Action: plans.Update, 1724 Mode: addrs.ManagedResourceMode, 1725 Before: cty.ObjectVal(map[string]cty.Value{ 1726 "id": cty.StringVal("i-02ae66f368e8518a9"), 1727 "ami": cty.StringVal("ami-STATIC"), 1728 "set_field": cty.SetVal([]cty.Value{ 1729 cty.StringVal("aaaa"), 1730 cty.StringVal("bbbb"), 1731 cty.StringVal("cccc"), 1732 }), 1733 }), 1734 After: cty.ObjectVal(map[string]cty.Value{ 1735 "id": cty.UnknownVal(cty.String), 1736 "ami": cty.StringVal("ami-STATIC"), 1737 "set_field": cty.SetVal([]cty.Value{ 1738 cty.StringVal("bbbb"), 1739 }), 1740 }), 1741 Schema: &configschema.Block{ 1742 Attributes: map[string]*configschema.Attribute{ 1743 "id": {Type: cty.String, Optional: true, Computed: true}, 1744 "ami": {Type: cty.String, Optional: true}, 1745 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1746 }, 1747 }, 1748 RequiredReplace: cty.NewPathSet(), 1749 ExpectedOutput: ` # test_instance.example will be updated in-place 1750 ~ resource "test_instance" "example" { 1751 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1752 ~ set_field = [ 1753 - "aaaa", 1754 - "cccc", 1755 # (1 unchanged element hidden) 1756 ] 1757 # (1 unchanged attribute hidden) 1758 } 1759`, 1760 }, 1761 "creation - empty set": { 1762 Action: plans.Create, 1763 Mode: addrs.ManagedResourceMode, 1764 Before: cty.NullVal(cty.EmptyObject), 1765 After: cty.ObjectVal(map[string]cty.Value{ 1766 "id": cty.UnknownVal(cty.String), 1767 "ami": cty.StringVal("ami-STATIC"), 1768 "set_field": cty.SetValEmpty(cty.String), 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 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1775 }, 1776 }, 1777 RequiredReplace: cty.NewPathSet(), 1778 ExpectedOutput: ` # test_instance.example will be created 1779 + resource "test_instance" "example" { 1780 + ami = "ami-STATIC" 1781 + id = (known after apply) 1782 + set_field = [] 1783 } 1784`, 1785 }, 1786 "in-place update - full to empty set": { 1787 Action: plans.Update, 1788 Mode: addrs.ManagedResourceMode, 1789 Before: cty.ObjectVal(map[string]cty.Value{ 1790 "id": cty.StringVal("i-02ae66f368e8518a9"), 1791 "ami": cty.StringVal("ami-STATIC"), 1792 "set_field": cty.SetVal([]cty.Value{ 1793 cty.StringVal("aaaa"), 1794 cty.StringVal("bbbb"), 1795 }), 1796 }), 1797 After: cty.ObjectVal(map[string]cty.Value{ 1798 "id": cty.UnknownVal(cty.String), 1799 "ami": cty.StringVal("ami-STATIC"), 1800 "set_field": cty.SetValEmpty(cty.String), 1801 }), 1802 Schema: &configschema.Block{ 1803 Attributes: map[string]*configschema.Attribute{ 1804 "id": {Type: cty.String, Optional: true, Computed: true}, 1805 "ami": {Type: cty.String, Optional: true}, 1806 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1807 }, 1808 }, 1809 RequiredReplace: cty.NewPathSet(), 1810 ExpectedOutput: ` # test_instance.example will be updated in-place 1811 ~ resource "test_instance" "example" { 1812 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1813 ~ set_field = [ 1814 - "aaaa", 1815 - "bbbb", 1816 ] 1817 # (1 unchanged attribute hidden) 1818 } 1819`, 1820 }, 1821 "in-place update - null to empty set": { 1822 Action: plans.Update, 1823 Mode: addrs.ManagedResourceMode, 1824 Before: cty.ObjectVal(map[string]cty.Value{ 1825 "id": cty.StringVal("i-02ae66f368e8518a9"), 1826 "ami": cty.StringVal("ami-STATIC"), 1827 "set_field": cty.NullVal(cty.Set(cty.String)), 1828 }), 1829 After: cty.ObjectVal(map[string]cty.Value{ 1830 "id": cty.UnknownVal(cty.String), 1831 "ami": cty.StringVal("ami-STATIC"), 1832 "set_field": cty.SetValEmpty(cty.String), 1833 }), 1834 Schema: &configschema.Block{ 1835 Attributes: map[string]*configschema.Attribute{ 1836 "id": {Type: cty.String, Optional: true, Computed: true}, 1837 "ami": {Type: cty.String, Optional: true}, 1838 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1839 }, 1840 }, 1841 RequiredReplace: cty.NewPathSet(), 1842 ExpectedOutput: ` # test_instance.example will be updated in-place 1843 ~ resource "test_instance" "example" { 1844 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1845 + set_field = [] 1846 # (1 unchanged attribute hidden) 1847 } 1848`, 1849 }, 1850 "in-place update to unknown": { 1851 Action: plans.Update, 1852 Mode: addrs.ManagedResourceMode, 1853 Before: cty.ObjectVal(map[string]cty.Value{ 1854 "id": cty.StringVal("i-02ae66f368e8518a9"), 1855 "ami": cty.StringVal("ami-STATIC"), 1856 "set_field": cty.SetVal([]cty.Value{ 1857 cty.StringVal("aaaa"), 1858 cty.StringVal("bbbb"), 1859 }), 1860 }), 1861 After: cty.ObjectVal(map[string]cty.Value{ 1862 "id": cty.UnknownVal(cty.String), 1863 "ami": cty.StringVal("ami-STATIC"), 1864 "set_field": cty.UnknownVal(cty.Set(cty.String)), 1865 }), 1866 Schema: &configschema.Block{ 1867 Attributes: map[string]*configschema.Attribute{ 1868 "id": {Type: cty.String, Optional: true, Computed: true}, 1869 "ami": {Type: cty.String, Optional: true}, 1870 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1871 }, 1872 }, 1873 RequiredReplace: cty.NewPathSet(), 1874 ExpectedOutput: ` # test_instance.example will be updated in-place 1875 ~ resource "test_instance" "example" { 1876 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1877 ~ set_field = [ 1878 - "aaaa", 1879 - "bbbb", 1880 ] -> (known after apply) 1881 # (1 unchanged attribute hidden) 1882 } 1883`, 1884 }, 1885 "in-place update to unknown element": { 1886 Action: plans.Update, 1887 Mode: addrs.ManagedResourceMode, 1888 Before: cty.ObjectVal(map[string]cty.Value{ 1889 "id": cty.StringVal("i-02ae66f368e8518a9"), 1890 "ami": cty.StringVal("ami-STATIC"), 1891 "set_field": cty.SetVal([]cty.Value{ 1892 cty.StringVal("aaaa"), 1893 cty.StringVal("bbbb"), 1894 }), 1895 }), 1896 After: cty.ObjectVal(map[string]cty.Value{ 1897 "id": cty.UnknownVal(cty.String), 1898 "ami": cty.StringVal("ami-STATIC"), 1899 "set_field": cty.SetVal([]cty.Value{ 1900 cty.StringVal("aaaa"), 1901 cty.UnknownVal(cty.String), 1902 }), 1903 }), 1904 Schema: &configschema.Block{ 1905 Attributes: map[string]*configschema.Attribute{ 1906 "id": {Type: cty.String, Optional: true, Computed: true}, 1907 "ami": {Type: cty.String, Optional: true}, 1908 "set_field": {Type: cty.Set(cty.String), Optional: true}, 1909 }, 1910 }, 1911 RequiredReplace: cty.NewPathSet(), 1912 ExpectedOutput: ` # test_instance.example will be updated in-place 1913 ~ resource "test_instance" "example" { 1914 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1915 ~ set_field = [ 1916 - "bbbb", 1917 ~ (known after apply), 1918 # (1 unchanged element hidden) 1919 ] 1920 # (1 unchanged attribute hidden) 1921 } 1922`, 1923 }, 1924 } 1925 runTestCases(t, testCases) 1926} 1927 1928func TestResourceChange_map(t *testing.T) { 1929 testCases := map[string]testCase{ 1930 "in-place update - creation": { 1931 Action: plans.Update, 1932 Mode: addrs.ManagedResourceMode, 1933 Before: cty.ObjectVal(map[string]cty.Value{ 1934 "id": cty.StringVal("i-02ae66f368e8518a9"), 1935 "ami": cty.StringVal("ami-STATIC"), 1936 "map_field": cty.NullVal(cty.Map(cty.String)), 1937 }), 1938 After: cty.ObjectVal(map[string]cty.Value{ 1939 "id": cty.UnknownVal(cty.String), 1940 "ami": cty.StringVal("ami-STATIC"), 1941 "map_field": cty.MapVal(map[string]cty.Value{ 1942 "new-key": cty.StringVal("new-element"), 1943 }), 1944 }), 1945 Schema: &configschema.Block{ 1946 Attributes: map[string]*configschema.Attribute{ 1947 "id": {Type: cty.String, Optional: true, Computed: true}, 1948 "ami": {Type: cty.String, Optional: true}, 1949 "map_field": {Type: cty.Map(cty.String), Optional: true}, 1950 }, 1951 }, 1952 RequiredReplace: cty.NewPathSet(), 1953 ExpectedOutput: ` # test_instance.example will be updated in-place 1954 ~ resource "test_instance" "example" { 1955 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1956 + map_field = { 1957 + "new-key" = "new-element" 1958 } 1959 # (1 unchanged attribute hidden) 1960 } 1961`, 1962 }, 1963 "in-place update - first insertion": { 1964 Action: plans.Update, 1965 Mode: addrs.ManagedResourceMode, 1966 Before: cty.ObjectVal(map[string]cty.Value{ 1967 "id": cty.StringVal("i-02ae66f368e8518a9"), 1968 "ami": cty.StringVal("ami-STATIC"), 1969 "map_field": cty.MapValEmpty(cty.String), 1970 }), 1971 After: cty.ObjectVal(map[string]cty.Value{ 1972 "id": cty.UnknownVal(cty.String), 1973 "ami": cty.StringVal("ami-STATIC"), 1974 "map_field": cty.MapVal(map[string]cty.Value{ 1975 "new-key": cty.StringVal("new-element"), 1976 }), 1977 }), 1978 Schema: &configschema.Block{ 1979 Attributes: map[string]*configschema.Attribute{ 1980 "id": {Type: cty.String, Optional: true, Computed: true}, 1981 "ami": {Type: cty.String, Optional: true}, 1982 "map_field": {Type: cty.Map(cty.String), Optional: true}, 1983 }, 1984 }, 1985 RequiredReplace: cty.NewPathSet(), 1986 ExpectedOutput: ` # test_instance.example will be updated in-place 1987 ~ resource "test_instance" "example" { 1988 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 1989 ~ map_field = { 1990 + "new-key" = "new-element" 1991 } 1992 # (1 unchanged attribute hidden) 1993 } 1994`, 1995 }, 1996 "in-place update - insertion": { 1997 Action: plans.Update, 1998 Mode: addrs.ManagedResourceMode, 1999 Before: cty.ObjectVal(map[string]cty.Value{ 2000 "id": cty.StringVal("i-02ae66f368e8518a9"), 2001 "ami": cty.StringVal("ami-STATIC"), 2002 "map_field": cty.MapVal(map[string]cty.Value{ 2003 "a": cty.StringVal("aaaa"), 2004 "c": cty.StringVal("cccc"), 2005 }), 2006 }), 2007 After: cty.ObjectVal(map[string]cty.Value{ 2008 "id": cty.UnknownVal(cty.String), 2009 "ami": cty.StringVal("ami-STATIC"), 2010 "map_field": cty.MapVal(map[string]cty.Value{ 2011 "a": cty.StringVal("aaaa"), 2012 "b": cty.StringVal("bbbb"), 2013 "c": cty.StringVal("cccc"), 2014 }), 2015 }), 2016 Schema: &configschema.Block{ 2017 Attributes: map[string]*configschema.Attribute{ 2018 "id": {Type: cty.String, Optional: true, Computed: true}, 2019 "ami": {Type: cty.String, Optional: true}, 2020 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2021 }, 2022 }, 2023 RequiredReplace: cty.NewPathSet(), 2024 ExpectedOutput: ` # test_instance.example will be updated in-place 2025 ~ resource "test_instance" "example" { 2026 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2027 ~ map_field = { 2028 + "b" = "bbbb" 2029 # (2 unchanged elements hidden) 2030 } 2031 # (1 unchanged attribute hidden) 2032 } 2033`, 2034 }, 2035 "force-new update - insertion": { 2036 Action: plans.DeleteThenCreate, 2037 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 2038 Mode: addrs.ManagedResourceMode, 2039 Before: cty.ObjectVal(map[string]cty.Value{ 2040 "id": cty.StringVal("i-02ae66f368e8518a9"), 2041 "ami": cty.StringVal("ami-STATIC"), 2042 "map_field": cty.MapVal(map[string]cty.Value{ 2043 "a": cty.StringVal("aaaa"), 2044 "c": cty.StringVal("cccc"), 2045 }), 2046 }), 2047 After: cty.ObjectVal(map[string]cty.Value{ 2048 "id": cty.UnknownVal(cty.String), 2049 "ami": cty.StringVal("ami-STATIC"), 2050 "map_field": cty.MapVal(map[string]cty.Value{ 2051 "a": cty.StringVal("aaaa"), 2052 "b": cty.StringVal("bbbb"), 2053 "c": cty.StringVal("cccc"), 2054 }), 2055 }), 2056 Schema: &configschema.Block{ 2057 Attributes: map[string]*configschema.Attribute{ 2058 "id": {Type: cty.String, Optional: true, Computed: true}, 2059 "ami": {Type: cty.String, Optional: true}, 2060 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2061 }, 2062 }, 2063 RequiredReplace: cty.NewPathSet(cty.Path{ 2064 cty.GetAttrStep{Name: "map_field"}, 2065 }), 2066 ExpectedOutput: ` # test_instance.example must be replaced 2067-/+ resource "test_instance" "example" { 2068 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2069 ~ map_field = { # forces replacement 2070 + "b" = "bbbb" 2071 # (2 unchanged elements hidden) 2072 } 2073 # (1 unchanged attribute hidden) 2074 } 2075`, 2076 }, 2077 "in-place update - deletion": { 2078 Action: plans.Update, 2079 Mode: addrs.ManagedResourceMode, 2080 Before: cty.ObjectVal(map[string]cty.Value{ 2081 "id": cty.StringVal("i-02ae66f368e8518a9"), 2082 "ami": cty.StringVal("ami-STATIC"), 2083 "map_field": cty.MapVal(map[string]cty.Value{ 2084 "a": cty.StringVal("aaaa"), 2085 "b": cty.StringVal("bbbb"), 2086 "c": cty.StringVal("cccc"), 2087 }), 2088 }), 2089 After: cty.ObjectVal(map[string]cty.Value{ 2090 "id": cty.UnknownVal(cty.String), 2091 "ami": cty.StringVal("ami-STATIC"), 2092 "map_field": cty.MapVal(map[string]cty.Value{ 2093 "b": cty.StringVal("bbbb"), 2094 }), 2095 }), 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 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2101 }, 2102 }, 2103 RequiredReplace: cty.NewPathSet(), 2104 ExpectedOutput: ` # test_instance.example will be updated in-place 2105 ~ resource "test_instance" "example" { 2106 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2107 ~ map_field = { 2108 - "a" = "aaaa" -> null 2109 - "c" = "cccc" -> null 2110 # (1 unchanged element hidden) 2111 } 2112 # (1 unchanged attribute hidden) 2113 } 2114`, 2115 }, 2116 "creation - empty": { 2117 Action: plans.Create, 2118 Mode: addrs.ManagedResourceMode, 2119 Before: cty.NullVal(cty.EmptyObject), 2120 After: cty.ObjectVal(map[string]cty.Value{ 2121 "id": cty.UnknownVal(cty.String), 2122 "ami": cty.StringVal("ami-STATIC"), 2123 "map_field": cty.MapValEmpty(cty.String), 2124 }), 2125 Schema: &configschema.Block{ 2126 Attributes: map[string]*configschema.Attribute{ 2127 "id": {Type: cty.String, Optional: true, Computed: true}, 2128 "ami": {Type: cty.String, Optional: true}, 2129 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2130 }, 2131 }, 2132 RequiredReplace: cty.NewPathSet(), 2133 ExpectedOutput: ` # test_instance.example will be created 2134 + resource "test_instance" "example" { 2135 + ami = "ami-STATIC" 2136 + id = (known after apply) 2137 + map_field = {} 2138 } 2139`, 2140 }, 2141 "update to unknown element": { 2142 Action: plans.Update, 2143 Mode: addrs.ManagedResourceMode, 2144 Before: cty.ObjectVal(map[string]cty.Value{ 2145 "id": cty.StringVal("i-02ae66f368e8518a9"), 2146 "ami": cty.StringVal("ami-STATIC"), 2147 "map_field": cty.MapVal(map[string]cty.Value{ 2148 "a": cty.StringVal("aaaa"), 2149 "b": cty.StringVal("bbbb"), 2150 "c": cty.StringVal("cccc"), 2151 }), 2152 }), 2153 After: cty.ObjectVal(map[string]cty.Value{ 2154 "id": cty.UnknownVal(cty.String), 2155 "ami": cty.StringVal("ami-STATIC"), 2156 "map_field": cty.MapVal(map[string]cty.Value{ 2157 "a": cty.StringVal("aaaa"), 2158 "b": cty.UnknownVal(cty.String), 2159 "c": cty.StringVal("cccc"), 2160 }), 2161 }), 2162 Schema: &configschema.Block{ 2163 Attributes: map[string]*configschema.Attribute{ 2164 "id": {Type: cty.String, Optional: true, Computed: true}, 2165 "ami": {Type: cty.String, Optional: true}, 2166 "map_field": {Type: cty.Map(cty.String), Optional: true}, 2167 }, 2168 }, 2169 RequiredReplace: cty.NewPathSet(), 2170 ExpectedOutput: ` # test_instance.example will be updated in-place 2171 ~ resource "test_instance" "example" { 2172 ~ id = "i-02ae66f368e8518a9" -> (known after apply) 2173 ~ map_field = { 2174 ~ "b" = "bbbb" -> (known after apply) 2175 # (2 unchanged elements hidden) 2176 } 2177 # (1 unchanged attribute hidden) 2178 } 2179`, 2180 }, 2181 } 2182 runTestCases(t, testCases) 2183} 2184 2185func TestResourceChange_nestedList(t *testing.T) { 2186 testCases := map[string]testCase{ 2187 "in-place update - equal": { 2188 Action: plans.Update, 2189 Mode: addrs.ManagedResourceMode, 2190 Before: cty.ObjectVal(map[string]cty.Value{ 2191 "id": cty.StringVal("i-02ae66f368e8518a9"), 2192 "ami": cty.StringVal("ami-BEFORE"), 2193 "root_block_device": cty.ListVal([]cty.Value{ 2194 cty.ObjectVal(map[string]cty.Value{ 2195 "volume_type": cty.StringVal("gp2"), 2196 }), 2197 }), 2198 "disks": cty.ListVal([]cty.Value{ 2199 cty.ObjectVal(map[string]cty.Value{ 2200 "mount_point": cty.StringVal("/var/diska"), 2201 "size": cty.StringVal("50GB"), 2202 }), 2203 }), 2204 }), 2205 After: cty.ObjectVal(map[string]cty.Value{ 2206 "id": cty.StringVal("i-02ae66f368e8518a9"), 2207 "ami": cty.StringVal("ami-AFTER"), 2208 "root_block_device": cty.ListVal([]cty.Value{ 2209 cty.ObjectVal(map[string]cty.Value{ 2210 "volume_type": cty.StringVal("gp2"), 2211 }), 2212 }), 2213 "disks": cty.ListVal([]cty.Value{ 2214 cty.ObjectVal(map[string]cty.Value{ 2215 "mount_point": cty.StringVal("/var/diska"), 2216 "size": cty.StringVal("50GB"), 2217 }), 2218 }), 2219 }), 2220 RequiredReplace: cty.NewPathSet(), 2221 Schema: testSchema(configschema.NestingList), 2222 ExpectedOutput: ` # test_instance.example will be updated in-place 2223 ~ resource "test_instance" "example" { 2224 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2225 id = "i-02ae66f368e8518a9" 2226 # (1 unchanged attribute hidden) 2227 2228 # (1 unchanged block hidden) 2229 } 2230`, 2231 }, 2232 "in-place update - creation": { 2233 Action: plans.Update, 2234 Mode: addrs.ManagedResourceMode, 2235 Before: cty.ObjectVal(map[string]cty.Value{ 2236 "id": cty.StringVal("i-02ae66f368e8518a9"), 2237 "ami": cty.StringVal("ami-BEFORE"), 2238 "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2239 "volume_type": cty.String, 2240 })), 2241 "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2242 "mount_point": cty.String, 2243 "size": cty.String, 2244 })), 2245 }), 2246 After: cty.ObjectVal(map[string]cty.Value{ 2247 "id": cty.StringVal("i-02ae66f368e8518a9"), 2248 "ami": cty.StringVal("ami-AFTER"), 2249 "disks": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ 2250 "mount_point": cty.StringVal("/var/diska"), 2251 "size": cty.StringVal("50GB"), 2252 })}), 2253 "root_block_device": cty.ListVal([]cty.Value{ 2254 cty.ObjectVal(map[string]cty.Value{ 2255 "volume_type": cty.NullVal(cty.String), 2256 }), 2257 }), 2258 }), 2259 RequiredReplace: cty.NewPathSet(), 2260 Schema: testSchema(configschema.NestingList), 2261 ExpectedOutput: ` # test_instance.example will be updated in-place 2262 ~ resource "test_instance" "example" { 2263 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2264 ~ disks = [ 2265 ~ { 2266 + mount_point = "/var/diska" 2267 + size = "50GB" 2268 }, 2269 ] 2270 id = "i-02ae66f368e8518a9" 2271 2272 + root_block_device {} 2273 } 2274`, 2275 }, 2276 "in-place update - first insertion": { 2277 Action: plans.Update, 2278 Mode: addrs.ManagedResourceMode, 2279 Before: cty.ObjectVal(map[string]cty.Value{ 2280 "id": cty.StringVal("i-02ae66f368e8518a9"), 2281 "ami": cty.StringVal("ami-BEFORE"), 2282 "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2283 "volume_type": cty.String, 2284 })), 2285 "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2286 "mount_point": cty.String, 2287 "size": cty.String, 2288 })), 2289 }), 2290 After: cty.ObjectVal(map[string]cty.Value{ 2291 "id": cty.StringVal("i-02ae66f368e8518a9"), 2292 "ami": cty.StringVal("ami-AFTER"), 2293 "disks": cty.ListVal([]cty.Value{ 2294 cty.ObjectVal(map[string]cty.Value{ 2295 "mount_point": cty.StringVal("/var/diska"), 2296 "size": cty.NullVal(cty.String), 2297 }), 2298 }), 2299 "root_block_device": cty.ListVal([]cty.Value{ 2300 cty.ObjectVal(map[string]cty.Value{ 2301 "volume_type": cty.StringVal("gp2"), 2302 }), 2303 }), 2304 }), 2305 RequiredReplace: cty.NewPathSet(), 2306 Schema: testSchema(configschema.NestingList), 2307 ExpectedOutput: ` # test_instance.example will be updated in-place 2308 ~ resource "test_instance" "example" { 2309 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2310 ~ disks = [ 2311 ~ { 2312 + mount_point = "/var/diska" 2313 }, 2314 ] 2315 id = "i-02ae66f368e8518a9" 2316 2317 + root_block_device { 2318 + volume_type = "gp2" 2319 } 2320 } 2321`, 2322 }, 2323 "in-place update - insertion": { 2324 Action: plans.Update, 2325 Mode: addrs.ManagedResourceMode, 2326 Before: cty.ObjectVal(map[string]cty.Value{ 2327 "id": cty.StringVal("i-02ae66f368e8518a9"), 2328 "ami": cty.StringVal("ami-BEFORE"), 2329 "disks": cty.ListVal([]cty.Value{ 2330 cty.ObjectVal(map[string]cty.Value{ 2331 "mount_point": cty.StringVal("/var/diska"), 2332 "size": cty.NullVal(cty.String), 2333 }), 2334 cty.ObjectVal(map[string]cty.Value{ 2335 "mount_point": cty.StringVal("/var/diskb"), 2336 "size": cty.StringVal("50GB"), 2337 }), 2338 }), 2339 "root_block_device": cty.ListVal([]cty.Value{ 2340 cty.ObjectVal(map[string]cty.Value{ 2341 "volume_type": cty.StringVal("gp2"), 2342 "new_field": cty.NullVal(cty.String), 2343 }), 2344 }), 2345 }), 2346 After: cty.ObjectVal(map[string]cty.Value{ 2347 "id": cty.StringVal("i-02ae66f368e8518a9"), 2348 "ami": cty.StringVal("ami-AFTER"), 2349 "disks": cty.ListVal([]cty.Value{ 2350 cty.ObjectVal(map[string]cty.Value{ 2351 "mount_point": cty.StringVal("/var/diska"), 2352 "size": cty.StringVal("50GB"), 2353 }), 2354 cty.ObjectVal(map[string]cty.Value{ 2355 "mount_point": cty.StringVal("/var/diskb"), 2356 "size": cty.StringVal("50GB"), 2357 }), 2358 }), 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 RequiredReplace: cty.NewPathSet(), 2367 Schema: testSchemaPlus(configschema.NestingList), 2368 ExpectedOutput: ` # test_instance.example will be updated in-place 2369 ~ resource "test_instance" "example" { 2370 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2371 ~ disks = [ 2372 ~ { 2373 + size = "50GB" 2374 # (1 unchanged attribute hidden) 2375 }, 2376 # (1 unchanged element hidden) 2377 ] 2378 id = "i-02ae66f368e8518a9" 2379 2380 ~ root_block_device { 2381 + new_field = "new_value" 2382 # (1 unchanged attribute hidden) 2383 } 2384 } 2385`, 2386 }, 2387 "force-new update (inside blocks)": { 2388 Action: plans.DeleteThenCreate, 2389 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 2390 Mode: addrs.ManagedResourceMode, 2391 Before: cty.ObjectVal(map[string]cty.Value{ 2392 "id": cty.StringVal("i-02ae66f368e8518a9"), 2393 "ami": cty.StringVal("ami-BEFORE"), 2394 "disks": cty.ListVal([]cty.Value{ 2395 cty.ObjectVal(map[string]cty.Value{ 2396 "mount_point": cty.StringVal("/var/diska"), 2397 "size": cty.StringVal("50GB"), 2398 }), 2399 }), 2400 "root_block_device": cty.ListVal([]cty.Value{ 2401 cty.ObjectVal(map[string]cty.Value{ 2402 "volume_type": cty.StringVal("gp2"), 2403 }), 2404 }), 2405 }), 2406 After: cty.ObjectVal(map[string]cty.Value{ 2407 "id": cty.StringVal("i-02ae66f368e8518a9"), 2408 "ami": cty.StringVal("ami-AFTER"), 2409 "disks": cty.ListVal([]cty.Value{ 2410 cty.ObjectVal(map[string]cty.Value{ 2411 "mount_point": cty.StringVal("/var/diskb"), 2412 "size": cty.StringVal("50GB"), 2413 }), 2414 }), 2415 "root_block_device": cty.ListVal([]cty.Value{ 2416 cty.ObjectVal(map[string]cty.Value{ 2417 "volume_type": cty.StringVal("different"), 2418 }), 2419 }), 2420 }), 2421 RequiredReplace: cty.NewPathSet( 2422 cty.Path{ 2423 cty.GetAttrStep{Name: "root_block_device"}, 2424 cty.IndexStep{Key: cty.NumberIntVal(0)}, 2425 cty.GetAttrStep{Name: "volume_type"}, 2426 }, 2427 cty.Path{ 2428 cty.GetAttrStep{Name: "disks"}, 2429 cty.IndexStep{Key: cty.NumberIntVal(0)}, 2430 cty.GetAttrStep{Name: "mount_point"}, 2431 }, 2432 ), 2433 Schema: testSchema(configschema.NestingList), 2434 ExpectedOutput: ` # test_instance.example must be replaced 2435-/+ resource "test_instance" "example" { 2436 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2437 ~ disks = [ 2438 ~ { 2439 ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement 2440 # (1 unchanged attribute hidden) 2441 }, 2442 ] 2443 id = "i-02ae66f368e8518a9" 2444 2445 ~ root_block_device { 2446 ~ volume_type = "gp2" -> "different" # forces replacement 2447 } 2448 } 2449`, 2450 }, 2451 "force-new update (whole block)": { 2452 Action: plans.DeleteThenCreate, 2453 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 2454 Mode: addrs.ManagedResourceMode, 2455 Before: cty.ObjectVal(map[string]cty.Value{ 2456 "id": cty.StringVal("i-02ae66f368e8518a9"), 2457 "ami": cty.StringVal("ami-BEFORE"), 2458 "disks": cty.ListVal([]cty.Value{ 2459 cty.ObjectVal(map[string]cty.Value{ 2460 "mount_point": cty.StringVal("/var/diska"), 2461 "size": cty.StringVal("50GB"), 2462 }), 2463 }), 2464 "root_block_device": cty.ListVal([]cty.Value{ 2465 cty.ObjectVal(map[string]cty.Value{ 2466 "volume_type": cty.StringVal("gp2"), 2467 }), 2468 }), 2469 }), 2470 After: cty.ObjectVal(map[string]cty.Value{ 2471 "id": cty.StringVal("i-02ae66f368e8518a9"), 2472 "ami": cty.StringVal("ami-AFTER"), 2473 "disks": cty.ListVal([]cty.Value{ 2474 cty.ObjectVal(map[string]cty.Value{ 2475 "mount_point": cty.StringVal("/var/diskb"), 2476 "size": cty.StringVal("50GB"), 2477 }), 2478 }), 2479 "root_block_device": cty.ListVal([]cty.Value{ 2480 cty.ObjectVal(map[string]cty.Value{ 2481 "volume_type": cty.StringVal("different"), 2482 }), 2483 }), 2484 }), 2485 RequiredReplace: cty.NewPathSet( 2486 cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, 2487 cty.Path{cty.GetAttrStep{Name: "disks"}}, 2488 ), 2489 Schema: testSchema(configschema.NestingList), 2490 ExpectedOutput: ` # test_instance.example must be replaced 2491-/+ resource "test_instance" "example" { 2492 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2493 ~ disks = [ # forces replacement 2494 ~ { 2495 ~ mount_point = "/var/diska" -> "/var/diskb" 2496 # (1 unchanged attribute hidden) 2497 }, 2498 ] 2499 id = "i-02ae66f368e8518a9" 2500 2501 ~ root_block_device { # forces replacement 2502 ~ volume_type = "gp2" -> "different" 2503 } 2504 } 2505`, 2506 }, 2507 "in-place update - deletion": { 2508 Action: plans.Update, 2509 Mode: addrs.ManagedResourceMode, 2510 Before: cty.ObjectVal(map[string]cty.Value{ 2511 "id": cty.StringVal("i-02ae66f368e8518a9"), 2512 "ami": cty.StringVal("ami-BEFORE"), 2513 "disks": cty.ListVal([]cty.Value{ 2514 cty.ObjectVal(map[string]cty.Value{ 2515 "mount_point": cty.StringVal("/var/diska"), 2516 "size": cty.StringVal("50GB"), 2517 }), 2518 }), 2519 "root_block_device": cty.ListVal([]cty.Value{ 2520 cty.ObjectVal(map[string]cty.Value{ 2521 "volume_type": cty.StringVal("gp2"), 2522 }), 2523 }), 2524 }), 2525 After: cty.ObjectVal(map[string]cty.Value{ 2526 "id": cty.StringVal("i-02ae66f368e8518a9"), 2527 "ami": cty.StringVal("ami-AFTER"), 2528 "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2529 "mount_point": cty.String, 2530 "size": cty.String, 2531 })), 2532 "root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{ 2533 "volume_type": cty.String, 2534 })), 2535 }), 2536 RequiredReplace: cty.NewPathSet(), 2537 Schema: testSchema(configschema.NestingList), 2538 ExpectedOutput: ` # test_instance.example will be updated in-place 2539 ~ resource "test_instance" "example" { 2540 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2541 ~ disks = [ 2542 ~ { 2543 - mount_point = "/var/diska" -> null 2544 - size = "50GB" -> null 2545 }, 2546 ] 2547 id = "i-02ae66f368e8518a9" 2548 2549 - root_block_device { 2550 - volume_type = "gp2" -> null 2551 } 2552 } 2553`, 2554 }, 2555 "with dynamically-typed attribute": { 2556 Action: plans.Update, 2557 Mode: addrs.ManagedResourceMode, 2558 Before: cty.ObjectVal(map[string]cty.Value{ 2559 "block": cty.EmptyTupleVal, 2560 }), 2561 After: cty.ObjectVal(map[string]cty.Value{ 2562 "block": cty.TupleVal([]cty.Value{ 2563 cty.ObjectVal(map[string]cty.Value{ 2564 "attr": cty.StringVal("foo"), 2565 }), 2566 cty.ObjectVal(map[string]cty.Value{ 2567 "attr": cty.True, 2568 }), 2569 }), 2570 }), 2571 RequiredReplace: cty.NewPathSet(), 2572 Schema: &configschema.Block{ 2573 BlockTypes: map[string]*configschema.NestedBlock{ 2574 "block": { 2575 Block: configschema.Block{ 2576 Attributes: map[string]*configschema.Attribute{ 2577 "attr": {Type: cty.DynamicPseudoType, Optional: true}, 2578 }, 2579 }, 2580 Nesting: configschema.NestingList, 2581 }, 2582 }, 2583 }, 2584 ExpectedOutput: ` # test_instance.example will be updated in-place 2585 ~ resource "test_instance" "example" { 2586 + block { 2587 + attr = "foo" 2588 } 2589 + block { 2590 + attr = true 2591 } 2592 } 2593`, 2594 }, 2595 "in-place sequence update - deletion": { 2596 Action: plans.Update, 2597 Mode: addrs.ManagedResourceMode, 2598 Before: cty.ObjectVal(map[string]cty.Value{ 2599 "list": cty.ListVal([]cty.Value{ 2600 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("x")}), 2601 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}), 2602 }), 2603 }), 2604 After: cty.ObjectVal(map[string]cty.Value{ 2605 "list": cty.ListVal([]cty.Value{ 2606 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}), 2607 cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("z")}), 2608 }), 2609 }), 2610 RequiredReplace: cty.NewPathSet(), 2611 Schema: &configschema.Block{ 2612 BlockTypes: map[string]*configschema.NestedBlock{ 2613 "list": { 2614 Block: configschema.Block{ 2615 Attributes: map[string]*configschema.Attribute{ 2616 "attr": { 2617 Type: cty.String, 2618 Required: true, 2619 }, 2620 }, 2621 }, 2622 Nesting: configschema.NestingList, 2623 }, 2624 }, 2625 }, 2626 ExpectedOutput: ` # test_instance.example will be updated in-place 2627 ~ resource "test_instance" "example" { 2628 ~ list { 2629 ~ attr = "x" -> "y" 2630 } 2631 ~ list { 2632 ~ attr = "y" -> "z" 2633 } 2634 } 2635`, 2636 }, 2637 "in-place update - unknown": { 2638 Action: plans.Update, 2639 Mode: addrs.ManagedResourceMode, 2640 Before: cty.ObjectVal(map[string]cty.Value{ 2641 "id": cty.StringVal("i-02ae66f368e8518a9"), 2642 "ami": cty.StringVal("ami-BEFORE"), 2643 "disks": cty.ListVal([]cty.Value{ 2644 cty.ObjectVal(map[string]cty.Value{ 2645 "mount_point": cty.StringVal("/var/diska"), 2646 "size": cty.StringVal("50GB"), 2647 }), 2648 }), 2649 "root_block_device": cty.ListVal([]cty.Value{ 2650 cty.ObjectVal(map[string]cty.Value{ 2651 "volume_type": cty.StringVal("gp2"), 2652 "new_field": cty.StringVal("new_value"), 2653 }), 2654 }), 2655 }), 2656 After: cty.ObjectVal(map[string]cty.Value{ 2657 "id": cty.StringVal("i-02ae66f368e8518a9"), 2658 "ami": cty.StringVal("ami-AFTER"), 2659 "disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ 2660 "mount_point": cty.String, 2661 "size": cty.String, 2662 }))), 2663 "root_block_device": cty.ListVal([]cty.Value{ 2664 cty.ObjectVal(map[string]cty.Value{ 2665 "volume_type": cty.StringVal("gp2"), 2666 "new_field": cty.StringVal("new_value"), 2667 }), 2668 }), 2669 }), 2670 RequiredReplace: cty.NewPathSet(), 2671 Schema: testSchemaPlus(configschema.NestingList), 2672 ExpectedOutput: ` # test_instance.example will be updated in-place 2673 ~ resource "test_instance" "example" { 2674 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2675 ~ disks = [ 2676 ~ { 2677 - mount_point = "/var/diska" -> null 2678 - size = "50GB" -> null 2679 }, 2680 ] -> (known after apply) 2681 id = "i-02ae66f368e8518a9" 2682 2683 # (1 unchanged block hidden) 2684 } 2685`, 2686 }, 2687 } 2688 runTestCases(t, testCases) 2689} 2690 2691func TestResourceChange_nestedSet(t *testing.T) { 2692 testCases := map[string]testCase{ 2693 "in-place update - creation": { 2694 Action: plans.Update, 2695 Mode: addrs.ManagedResourceMode, 2696 Before: cty.ObjectVal(map[string]cty.Value{ 2697 "id": cty.StringVal("i-02ae66f368e8518a9"), 2698 "ami": cty.StringVal("ami-BEFORE"), 2699 "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 2700 "mount_point": cty.String, 2701 "size": cty.String, 2702 })), 2703 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 2704 "volume_type": cty.String, 2705 })), 2706 }), 2707 After: cty.ObjectVal(map[string]cty.Value{ 2708 "id": cty.StringVal("i-02ae66f368e8518a9"), 2709 "ami": cty.StringVal("ami-AFTER"), 2710 "disks": cty.SetVal([]cty.Value{ 2711 cty.ObjectVal(map[string]cty.Value{ 2712 "mount_point": cty.StringVal("/var/diska"), 2713 "size": cty.NullVal(cty.String), 2714 }), 2715 }), 2716 "root_block_device": cty.SetVal([]cty.Value{ 2717 cty.ObjectVal(map[string]cty.Value{ 2718 "volume_type": cty.StringVal("gp2"), 2719 }), 2720 }), 2721 }), 2722 RequiredReplace: cty.NewPathSet(), 2723 Schema: testSchema(configschema.NestingSet), 2724 ExpectedOutput: ` # test_instance.example will be updated in-place 2725 ~ resource "test_instance" "example" { 2726 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2727 ~ disks = [ 2728 + { 2729 + mount_point = "/var/diska" 2730 }, 2731 ] 2732 id = "i-02ae66f368e8518a9" 2733 2734 + root_block_device { 2735 + volume_type = "gp2" 2736 } 2737 } 2738`, 2739 }, 2740 "in-place update - insertion": { 2741 Action: plans.Update, 2742 Mode: addrs.ManagedResourceMode, 2743 Before: cty.ObjectVal(map[string]cty.Value{ 2744 "id": cty.StringVal("i-02ae66f368e8518a9"), 2745 "ami": cty.StringVal("ami-BEFORE"), 2746 "disks": cty.SetVal([]cty.Value{ 2747 cty.ObjectVal(map[string]cty.Value{ 2748 "mount_point": cty.StringVal("/var/diska"), 2749 "size": cty.NullVal(cty.String), 2750 }), 2751 }), 2752 "root_block_device": cty.SetVal([]cty.Value{ 2753 cty.ObjectVal(map[string]cty.Value{ 2754 "volume_type": cty.StringVal("gp2"), 2755 "new_field": cty.NullVal(cty.String), 2756 }), 2757 }), 2758 }), 2759 After: cty.ObjectVal(map[string]cty.Value{ 2760 "id": cty.StringVal("i-02ae66f368e8518a9"), 2761 "ami": cty.StringVal("ami-AFTER"), 2762 "disks": cty.SetVal([]cty.Value{ 2763 cty.ObjectVal(map[string]cty.Value{ 2764 "mount_point": cty.StringVal("/var/diska"), 2765 "size": cty.StringVal("50GB"), 2766 }), 2767 }), 2768 "root_block_device": cty.SetVal([]cty.Value{ 2769 cty.ObjectVal(map[string]cty.Value{ 2770 "volume_type": cty.StringVal("gp2"), 2771 "new_field": cty.StringVal("new_value"), 2772 }), 2773 }), 2774 }), 2775 RequiredReplace: cty.NewPathSet(), 2776 Schema: testSchemaPlus(configschema.NestingSet), 2777 ExpectedOutput: ` # test_instance.example will be updated in-place 2778 ~ resource "test_instance" "example" { 2779 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2780 ~ disks = [ 2781 + { 2782 + mount_point = "/var/diska" 2783 + size = "50GB" 2784 }, 2785 - { 2786 - mount_point = "/var/diska" -> null 2787 }, 2788 ] 2789 id = "i-02ae66f368e8518a9" 2790 2791 + root_block_device { 2792 + new_field = "new_value" 2793 + volume_type = "gp2" 2794 } 2795 - root_block_device { 2796 - volume_type = "gp2" -> null 2797 } 2798 } 2799`, 2800 }, 2801 "force-new update (whole block)": { 2802 Action: plans.DeleteThenCreate, 2803 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 2804 Mode: addrs.ManagedResourceMode, 2805 Before: cty.ObjectVal(map[string]cty.Value{ 2806 "id": cty.StringVal("i-02ae66f368e8518a9"), 2807 "ami": cty.StringVal("ami-BEFORE"), 2808 "root_block_device": cty.SetVal([]cty.Value{ 2809 cty.ObjectVal(map[string]cty.Value{ 2810 "volume_type": cty.StringVal("gp2"), 2811 }), 2812 }), 2813 "disks": cty.SetVal([]cty.Value{ 2814 cty.ObjectVal(map[string]cty.Value{ 2815 "mount_point": cty.StringVal("/var/diska"), 2816 "size": cty.StringVal("50GB"), 2817 }), 2818 }), 2819 }), 2820 After: cty.ObjectVal(map[string]cty.Value{ 2821 "id": cty.StringVal("i-02ae66f368e8518a9"), 2822 "ami": cty.StringVal("ami-AFTER"), 2823 "root_block_device": cty.SetVal([]cty.Value{ 2824 cty.ObjectVal(map[string]cty.Value{ 2825 "volume_type": cty.StringVal("different"), 2826 }), 2827 }), 2828 "disks": cty.SetVal([]cty.Value{ 2829 cty.ObjectVal(map[string]cty.Value{ 2830 "mount_point": cty.StringVal("/var/diskb"), 2831 "size": cty.StringVal("50GB"), 2832 }), 2833 }), 2834 }), 2835 RequiredReplace: cty.NewPathSet( 2836 cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, 2837 cty.Path{cty.GetAttrStep{Name: "disks"}}, 2838 ), 2839 Schema: testSchema(configschema.NestingSet), 2840 ExpectedOutput: ` # test_instance.example must be replaced 2841-/+ resource "test_instance" "example" { 2842 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2843 ~ disks = [ 2844 - { # forces replacement 2845 - mount_point = "/var/diska" -> null 2846 - size = "50GB" -> null 2847 }, 2848 + { # forces replacement 2849 + mount_point = "/var/diskb" 2850 + size = "50GB" 2851 }, 2852 ] 2853 id = "i-02ae66f368e8518a9" 2854 2855 + root_block_device { # forces replacement 2856 + volume_type = "different" 2857 } 2858 - root_block_device { # forces replacement 2859 - volume_type = "gp2" -> null 2860 } 2861 } 2862`, 2863 }, 2864 "in-place update - deletion": { 2865 Action: plans.Update, 2866 Mode: addrs.ManagedResourceMode, 2867 Before: cty.ObjectVal(map[string]cty.Value{ 2868 "id": cty.StringVal("i-02ae66f368e8518a9"), 2869 "ami": cty.StringVal("ami-BEFORE"), 2870 "root_block_device": cty.SetVal([]cty.Value{ 2871 cty.ObjectVal(map[string]cty.Value{ 2872 "volume_type": cty.StringVal("gp2"), 2873 "new_field": cty.StringVal("new_value"), 2874 }), 2875 }), 2876 "disks": cty.SetVal([]cty.Value{ 2877 cty.ObjectVal(map[string]cty.Value{ 2878 "mount_point": cty.StringVal("/var/diska"), 2879 "size": cty.StringVal("50GB"), 2880 }), 2881 }), 2882 }), 2883 After: cty.ObjectVal(map[string]cty.Value{ 2884 "id": cty.StringVal("i-02ae66f368e8518a9"), 2885 "ami": cty.StringVal("ami-AFTER"), 2886 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 2887 "volume_type": cty.String, 2888 "new_field": cty.String, 2889 })), 2890 "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 2891 "mount_point": cty.String, 2892 "size": cty.String, 2893 })), 2894 }), 2895 RequiredReplace: cty.NewPathSet(), 2896 Schema: testSchemaPlus(configschema.NestingSet), 2897 ExpectedOutput: ` # test_instance.example will be updated in-place 2898 ~ resource "test_instance" "example" { 2899 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2900 ~ disks = [ 2901 - { 2902 - mount_point = "/var/diska" -> null 2903 - size = "50GB" -> null 2904 }, 2905 ] 2906 id = "i-02ae66f368e8518a9" 2907 2908 - root_block_device { 2909 - new_field = "new_value" -> null 2910 - volume_type = "gp2" -> null 2911 } 2912 } 2913`, 2914 }, 2915 "in-place update - empty nested sets": { 2916 Action: plans.Update, 2917 Mode: addrs.ManagedResourceMode, 2918 Before: cty.ObjectVal(map[string]cty.Value{ 2919 "id": cty.StringVal("i-02ae66f368e8518a9"), 2920 "ami": cty.StringVal("ami-BEFORE"), 2921 "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 2922 "mount_point": cty.String, 2923 "size": cty.String, 2924 }))), 2925 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 2926 "volume_type": cty.String, 2927 })), 2928 }), 2929 After: cty.ObjectVal(map[string]cty.Value{ 2930 "id": cty.StringVal("i-02ae66f368e8518a9"), 2931 "ami": cty.StringVal("ami-AFTER"), 2932 "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 2933 "mount_point": cty.String, 2934 "size": cty.String, 2935 })), 2936 "root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{ 2937 "volume_type": cty.String, 2938 })), 2939 }), 2940 RequiredReplace: cty.NewPathSet(), 2941 Schema: testSchema(configschema.NestingSet), 2942 ExpectedOutput: ` # test_instance.example will be updated in-place 2943 ~ resource "test_instance" "example" { 2944 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2945 + disks = [ 2946 ] 2947 id = "i-02ae66f368e8518a9" 2948 } 2949`, 2950 }, 2951 "in-place update - null insertion": { 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 "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ 2958 "mount_point": cty.String, 2959 "size": cty.String, 2960 }))), 2961 "root_block_device": cty.SetVal([]cty.Value{ 2962 cty.ObjectVal(map[string]cty.Value{ 2963 "volume_type": cty.StringVal("gp2"), 2964 "new_field": cty.NullVal(cty.String), 2965 }), 2966 }), 2967 }), 2968 After: cty.ObjectVal(map[string]cty.Value{ 2969 "id": cty.StringVal("i-02ae66f368e8518a9"), 2970 "ami": cty.StringVal("ami-AFTER"), 2971 "disks": cty.SetVal([]cty.Value{ 2972 cty.ObjectVal(map[string]cty.Value{ 2973 "mount_point": cty.StringVal("/var/diska"), 2974 "size": cty.StringVal("50GB"), 2975 }), 2976 }), 2977 "root_block_device": cty.SetVal([]cty.Value{ 2978 cty.ObjectVal(map[string]cty.Value{ 2979 "volume_type": cty.StringVal("gp2"), 2980 "new_field": cty.StringVal("new_value"), 2981 }), 2982 }), 2983 }), 2984 RequiredReplace: cty.NewPathSet(), 2985 Schema: testSchemaPlus(configschema.NestingSet), 2986 ExpectedOutput: ` # test_instance.example will be updated in-place 2987 ~ resource "test_instance" "example" { 2988 ~ ami = "ami-BEFORE" -> "ami-AFTER" 2989 + disks = [ 2990 + { 2991 + mount_point = "/var/diska" 2992 + size = "50GB" 2993 }, 2994 ] 2995 id = "i-02ae66f368e8518a9" 2996 2997 + root_block_device { 2998 + new_field = "new_value" 2999 + volume_type = "gp2" 3000 } 3001 - root_block_device { 3002 - volume_type = "gp2" -> null 3003 } 3004 } 3005`, 3006 }, 3007 "in-place update - unknown": { 3008 Action: plans.Update, 3009 Mode: addrs.ManagedResourceMode, 3010 Before: cty.ObjectVal(map[string]cty.Value{ 3011 "id": cty.StringVal("i-02ae66f368e8518a9"), 3012 "ami": cty.StringVal("ami-BEFORE"), 3013 "disks": cty.SetVal([]cty.Value{ 3014 cty.ObjectVal(map[string]cty.Value{ 3015 "mount_point": cty.StringVal("/var/diska"), 3016 "size": cty.StringVal("50GB"), 3017 }), 3018 }), 3019 "root_block_device": cty.SetVal([]cty.Value{ 3020 cty.ObjectVal(map[string]cty.Value{ 3021 "volume_type": cty.StringVal("gp2"), 3022 "new_field": cty.StringVal("new_value"), 3023 }), 3024 }), 3025 }), 3026 After: cty.ObjectVal(map[string]cty.Value{ 3027 "id": cty.StringVal("i-02ae66f368e8518a9"), 3028 "ami": cty.StringVal("ami-AFTER"), 3029 "disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ 3030 "mount_point": cty.String, 3031 "size": cty.String, 3032 }))), 3033 "root_block_device": cty.SetVal([]cty.Value{ 3034 cty.ObjectVal(map[string]cty.Value{ 3035 "volume_type": cty.StringVal("gp2"), 3036 "new_field": cty.StringVal("new_value"), 3037 }), 3038 }), 3039 }), 3040 RequiredReplace: cty.NewPathSet(), 3041 Schema: testSchemaPlus(configschema.NestingSet), 3042 ExpectedOutput: ` # test_instance.example will be updated in-place 3043 ~ resource "test_instance" "example" { 3044 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3045 ~ disks = [ 3046 - { 3047 - mount_point = "/var/diska" -> null 3048 - size = "50GB" -> null 3049 }, 3050 ] -> (known after apply) 3051 id = "i-02ae66f368e8518a9" 3052 3053 # (1 unchanged block hidden) 3054 } 3055`, 3056 }, 3057 } 3058 runTestCases(t, testCases) 3059} 3060 3061func TestResourceChange_nestedMap(t *testing.T) { 3062 testCases := map[string]testCase{ 3063 "creation from null": { 3064 Action: plans.Update, 3065 Mode: addrs.ManagedResourceMode, 3066 Before: cty.ObjectVal(map[string]cty.Value{ 3067 "id": cty.NullVal(cty.String), 3068 "ami": cty.NullVal(cty.String), 3069 "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ 3070 "mount_point": cty.String, 3071 "size": cty.String, 3072 }))), 3073 "root_block_device": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ 3074 "volume_type": cty.String, 3075 }))), 3076 }), 3077 After: cty.ObjectVal(map[string]cty.Value{ 3078 "id": cty.StringVal("i-02ae66f368e8518a9"), 3079 "ami": cty.StringVal("ami-AFTER"), 3080 "disks": cty.MapVal(map[string]cty.Value{ 3081 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3082 "mount_point": cty.StringVal("/var/diska"), 3083 "size": cty.NullVal(cty.String), 3084 }), 3085 }), 3086 "root_block_device": cty.MapVal(map[string]cty.Value{ 3087 "a": cty.ObjectVal(map[string]cty.Value{ 3088 "volume_type": cty.StringVal("gp2"), 3089 }), 3090 }), 3091 }), 3092 RequiredReplace: cty.NewPathSet(), 3093 Schema: testSchema(configschema.NestingMap), 3094 ExpectedOutput: ` # test_instance.example will be updated in-place 3095 ~ resource "test_instance" "example" { 3096 + ami = "ami-AFTER" 3097 + disks = { 3098 + "disk_a" = { 3099 + mount_point = "/var/diska" 3100 }, 3101 } 3102 + id = "i-02ae66f368e8518a9" 3103 3104 + root_block_device "a" { 3105 + volume_type = "gp2" 3106 } 3107 } 3108`, 3109 }, 3110 "in-place update - creation": { 3111 Action: plans.Update, 3112 Mode: addrs.ManagedResourceMode, 3113 Before: cty.ObjectVal(map[string]cty.Value{ 3114 "id": cty.StringVal("i-02ae66f368e8518a9"), 3115 "ami": cty.StringVal("ami-BEFORE"), 3116 "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 3117 "mount_point": cty.String, 3118 "size": cty.String, 3119 })), 3120 "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 3121 "volume_type": cty.String, 3122 })), 3123 }), 3124 After: cty.ObjectVal(map[string]cty.Value{ 3125 "id": cty.StringVal("i-02ae66f368e8518a9"), 3126 "ami": cty.StringVal("ami-AFTER"), 3127 "disks": cty.MapVal(map[string]cty.Value{ 3128 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3129 "mount_point": cty.StringVal("/var/diska"), 3130 "size": cty.NullVal(cty.String), 3131 }), 3132 }), 3133 "root_block_device": cty.MapVal(map[string]cty.Value{ 3134 "a": cty.ObjectVal(map[string]cty.Value{ 3135 "volume_type": cty.StringVal("gp2"), 3136 }), 3137 }), 3138 }), 3139 RequiredReplace: cty.NewPathSet(), 3140 Schema: testSchema(configschema.NestingMap), 3141 ExpectedOutput: ` # test_instance.example will be updated in-place 3142 ~ resource "test_instance" "example" { 3143 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3144 ~ disks = { 3145 + "disk_a" = { 3146 + mount_point = "/var/diska" 3147 }, 3148 } 3149 id = "i-02ae66f368e8518a9" 3150 3151 + root_block_device "a" { 3152 + volume_type = "gp2" 3153 } 3154 } 3155`, 3156 }, 3157 "in-place update - change attr": { 3158 Action: plans.Update, 3159 Mode: addrs.ManagedResourceMode, 3160 Before: cty.ObjectVal(map[string]cty.Value{ 3161 "id": cty.StringVal("i-02ae66f368e8518a9"), 3162 "ami": cty.StringVal("ami-BEFORE"), 3163 "disks": cty.MapVal(map[string]cty.Value{ 3164 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3165 "mount_point": cty.StringVal("/var/diska"), 3166 "size": cty.NullVal(cty.String), 3167 }), 3168 }), 3169 "root_block_device": cty.MapVal(map[string]cty.Value{ 3170 "a": cty.ObjectVal(map[string]cty.Value{ 3171 "volume_type": cty.StringVal("gp2"), 3172 "new_field": cty.NullVal(cty.String), 3173 }), 3174 }), 3175 }), 3176 After: cty.ObjectVal(map[string]cty.Value{ 3177 "id": cty.StringVal("i-02ae66f368e8518a9"), 3178 "ami": cty.StringVal("ami-AFTER"), 3179 "disks": cty.MapVal(map[string]cty.Value{ 3180 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3181 "mount_point": cty.StringVal("/var/diska"), 3182 "size": cty.StringVal("50GB"), 3183 }), 3184 }), 3185 "root_block_device": cty.MapVal(map[string]cty.Value{ 3186 "a": cty.ObjectVal(map[string]cty.Value{ 3187 "volume_type": cty.StringVal("gp2"), 3188 "new_field": cty.StringVal("new_value"), 3189 }), 3190 }), 3191 }), 3192 RequiredReplace: cty.NewPathSet(), 3193 Schema: testSchemaPlus(configschema.NestingMap), 3194 ExpectedOutput: ` # test_instance.example will be updated in-place 3195 ~ resource "test_instance" "example" { 3196 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3197 ~ disks = { 3198 ~ "disk_a" = { 3199 + size = "50GB" 3200 # (1 unchanged attribute hidden) 3201 }, 3202 } 3203 id = "i-02ae66f368e8518a9" 3204 3205 ~ root_block_device "a" { 3206 + new_field = "new_value" 3207 # (1 unchanged attribute hidden) 3208 } 3209 } 3210`, 3211 }, 3212 "in-place update - insertion": { 3213 Action: plans.Update, 3214 Mode: addrs.ManagedResourceMode, 3215 Before: cty.ObjectVal(map[string]cty.Value{ 3216 "id": cty.StringVal("i-02ae66f368e8518a9"), 3217 "ami": cty.StringVal("ami-BEFORE"), 3218 "disks": cty.MapVal(map[string]cty.Value{ 3219 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3220 "mount_point": cty.StringVal("/var/diska"), 3221 "size": cty.StringVal("50GB"), 3222 }), 3223 }), 3224 "root_block_device": cty.MapVal(map[string]cty.Value{ 3225 "a": cty.ObjectVal(map[string]cty.Value{ 3226 "volume_type": cty.StringVal("gp2"), 3227 "new_field": cty.NullVal(cty.String), 3228 }), 3229 }), 3230 }), 3231 After: cty.ObjectVal(map[string]cty.Value{ 3232 "id": cty.StringVal("i-02ae66f368e8518a9"), 3233 "ami": cty.StringVal("ami-AFTER"), 3234 "disks": cty.MapVal(map[string]cty.Value{ 3235 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3236 "mount_point": cty.StringVal("/var/diska"), 3237 "size": cty.StringVal("50GB"), 3238 }), 3239 "disk_2": cty.ObjectVal(map[string]cty.Value{ 3240 "mount_point": cty.StringVal("/var/disk2"), 3241 "size": cty.StringVal("50GB"), 3242 }), 3243 }), 3244 "root_block_device": cty.MapVal(map[string]cty.Value{ 3245 "a": cty.ObjectVal(map[string]cty.Value{ 3246 "volume_type": cty.StringVal("gp2"), 3247 "new_field": cty.NullVal(cty.String), 3248 }), 3249 "b": cty.ObjectVal(map[string]cty.Value{ 3250 "volume_type": cty.StringVal("gp2"), 3251 "new_field": cty.StringVal("new_value"), 3252 }), 3253 }), 3254 }), 3255 RequiredReplace: cty.NewPathSet(), 3256 Schema: testSchemaPlus(configschema.NestingMap), 3257 ExpectedOutput: ` # test_instance.example will be updated in-place 3258 ~ resource "test_instance" "example" { 3259 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3260 ~ disks = { 3261 + "disk_2" = { 3262 + mount_point = "/var/disk2" 3263 + size = "50GB" 3264 }, 3265 # (1 unchanged element hidden) 3266 } 3267 id = "i-02ae66f368e8518a9" 3268 3269 + root_block_device "b" { 3270 + new_field = "new_value" 3271 + volume_type = "gp2" 3272 } 3273 # (1 unchanged block hidden) 3274 } 3275`, 3276 }, 3277 "force-new update (whole block)": { 3278 Action: plans.DeleteThenCreate, 3279 ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, 3280 Mode: addrs.ManagedResourceMode, 3281 Before: cty.ObjectVal(map[string]cty.Value{ 3282 "id": cty.StringVal("i-02ae66f368e8518a9"), 3283 "ami": cty.StringVal("ami-BEFORE"), 3284 "disks": cty.MapVal(map[string]cty.Value{ 3285 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3286 "mount_point": cty.StringVal("/var/diska"), 3287 "size": cty.StringVal("50GB"), 3288 }), 3289 }), 3290 "root_block_device": cty.MapVal(map[string]cty.Value{ 3291 "a": cty.ObjectVal(map[string]cty.Value{ 3292 "volume_type": cty.StringVal("gp2"), 3293 }), 3294 "b": cty.ObjectVal(map[string]cty.Value{ 3295 "volume_type": cty.StringVal("standard"), 3296 }), 3297 }), 3298 }), 3299 After: cty.ObjectVal(map[string]cty.Value{ 3300 "id": cty.StringVal("i-02ae66f368e8518a9"), 3301 "ami": cty.StringVal("ami-AFTER"), 3302 "disks": cty.MapVal(map[string]cty.Value{ 3303 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3304 "mount_point": cty.StringVal("/var/diska"), 3305 "size": cty.StringVal("100GB"), 3306 }), 3307 }), 3308 "root_block_device": cty.MapVal(map[string]cty.Value{ 3309 "a": cty.ObjectVal(map[string]cty.Value{ 3310 "volume_type": cty.StringVal("different"), 3311 }), 3312 "b": cty.ObjectVal(map[string]cty.Value{ 3313 "volume_type": cty.StringVal("standard"), 3314 }), 3315 }), 3316 }), 3317 RequiredReplace: cty.NewPathSet(cty.Path{ 3318 cty.GetAttrStep{Name: "root_block_device"}, 3319 cty.IndexStep{Key: cty.StringVal("a")}, 3320 }, 3321 cty.Path{cty.GetAttrStep{Name: "disks"}}, 3322 ), 3323 Schema: testSchema(configschema.NestingMap), 3324 ExpectedOutput: ` # test_instance.example must be replaced 3325-/+ resource "test_instance" "example" { 3326 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3327 ~ disks = { 3328 ~ "disk_a" = { # forces replacement 3329 ~ size = "50GB" -> "100GB" 3330 # (1 unchanged attribute hidden) 3331 }, 3332 } 3333 id = "i-02ae66f368e8518a9" 3334 3335 ~ root_block_device "a" { # forces replacement 3336 ~ volume_type = "gp2" -> "different" 3337 } 3338 # (1 unchanged block hidden) 3339 } 3340`, 3341 }, 3342 "in-place update - deletion": { 3343 Action: plans.Update, 3344 Mode: addrs.ManagedResourceMode, 3345 Before: cty.ObjectVal(map[string]cty.Value{ 3346 "id": cty.StringVal("i-02ae66f368e8518a9"), 3347 "ami": cty.StringVal("ami-BEFORE"), 3348 "disks": cty.MapVal(map[string]cty.Value{ 3349 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3350 "mount_point": cty.StringVal("/var/diska"), 3351 "size": cty.StringVal("50GB"), 3352 }), 3353 }), 3354 "root_block_device": cty.MapVal(map[string]cty.Value{ 3355 "a": cty.ObjectVal(map[string]cty.Value{ 3356 "volume_type": cty.StringVal("gp2"), 3357 "new_field": cty.StringVal("new_value"), 3358 }), 3359 }), 3360 }), 3361 After: cty.ObjectVal(map[string]cty.Value{ 3362 "id": cty.StringVal("i-02ae66f368e8518a9"), 3363 "ami": cty.StringVal("ami-AFTER"), 3364 "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 3365 "mount_point": cty.String, 3366 "size": cty.String, 3367 })), 3368 "root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 3369 "volume_type": cty.String, 3370 "new_field": cty.String, 3371 })), 3372 }), 3373 RequiredReplace: cty.NewPathSet(), 3374 Schema: testSchemaPlus(configschema.NestingMap), 3375 ExpectedOutput: ` # test_instance.example will be updated in-place 3376 ~ resource "test_instance" "example" { 3377 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3378 ~ disks = { 3379 - "disk_a" = { 3380 - mount_point = "/var/diska" -> null 3381 - size = "50GB" -> null 3382 }, 3383 } 3384 id = "i-02ae66f368e8518a9" 3385 3386 - root_block_device "a" { 3387 - new_field = "new_value" -> null 3388 - volume_type = "gp2" -> null 3389 } 3390 } 3391`, 3392 }, 3393 "in-place update - unknown": { 3394 Action: plans.Update, 3395 Mode: addrs.ManagedResourceMode, 3396 Before: cty.ObjectVal(map[string]cty.Value{ 3397 "id": cty.StringVal("i-02ae66f368e8518a9"), 3398 "ami": cty.StringVal("ami-BEFORE"), 3399 "disks": cty.MapVal(map[string]cty.Value{ 3400 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3401 "mount_point": cty.StringVal("/var/diska"), 3402 "size": cty.StringVal("50GB"), 3403 }), 3404 }), 3405 "root_block_device": cty.MapVal(map[string]cty.Value{ 3406 "a": cty.ObjectVal(map[string]cty.Value{ 3407 "volume_type": cty.StringVal("gp2"), 3408 "new_field": cty.StringVal("new_value"), 3409 }), 3410 }), 3411 }), 3412 After: cty.ObjectVal(map[string]cty.Value{ 3413 "id": cty.StringVal("i-02ae66f368e8518a9"), 3414 "ami": cty.StringVal("ami-AFTER"), 3415 "disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ 3416 "mount_point": cty.String, 3417 "size": cty.String, 3418 }))), 3419 "root_block_device": cty.MapVal(map[string]cty.Value{ 3420 "a": cty.ObjectVal(map[string]cty.Value{ 3421 "volume_type": cty.StringVal("gp2"), 3422 "new_field": cty.StringVal("new_value"), 3423 }), 3424 }), 3425 }), 3426 RequiredReplace: cty.NewPathSet(), 3427 Schema: testSchemaPlus(configschema.NestingMap), 3428 ExpectedOutput: ` # test_instance.example will be updated in-place 3429 ~ resource "test_instance" "example" { 3430 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3431 ~ disks = { 3432 - "disk_a" = { 3433 - mount_point = "/var/diska" -> null 3434 - size = "50GB" -> null 3435 }, 3436 } -> (known after apply) 3437 id = "i-02ae66f368e8518a9" 3438 3439 # (1 unchanged block hidden) 3440 } 3441`, 3442 }, 3443 "in-place update - insertion sensitive": { 3444 Action: plans.Update, 3445 Mode: addrs.ManagedResourceMode, 3446 Before: cty.ObjectVal(map[string]cty.Value{ 3447 "id": cty.StringVal("i-02ae66f368e8518a9"), 3448 "ami": cty.StringVal("ami-BEFORE"), 3449 "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ 3450 "mount_point": cty.String, 3451 "size": cty.String, 3452 })), 3453 "root_block_device": cty.MapVal(map[string]cty.Value{ 3454 "a": cty.ObjectVal(map[string]cty.Value{ 3455 "volume_type": cty.StringVal("gp2"), 3456 "new_field": cty.StringVal("new_value"), 3457 }), 3458 }), 3459 }), 3460 After: cty.ObjectVal(map[string]cty.Value{ 3461 "id": cty.StringVal("i-02ae66f368e8518a9"), 3462 "ami": cty.StringVal("ami-AFTER"), 3463 "disks": cty.MapVal(map[string]cty.Value{ 3464 "disk_a": cty.ObjectVal(map[string]cty.Value{ 3465 "mount_point": cty.StringVal("/var/diska"), 3466 "size": cty.StringVal("50GB"), 3467 }), 3468 }), 3469 "root_block_device": cty.MapVal(map[string]cty.Value{ 3470 "a": cty.ObjectVal(map[string]cty.Value{ 3471 "volume_type": cty.StringVal("gp2"), 3472 "new_field": cty.StringVal("new_value"), 3473 }), 3474 }), 3475 }), 3476 AfterValMarks: []cty.PathValueMarks{ 3477 { 3478 Path: cty.Path{cty.GetAttrStep{Name: "disks"}, 3479 cty.IndexStep{Key: cty.StringVal("disk_a")}, 3480 cty.GetAttrStep{Name: "mount_point"}, 3481 }, 3482 Marks: cty.NewValueMarks("sensitive"), 3483 }, 3484 }, 3485 RequiredReplace: cty.NewPathSet(), 3486 Schema: testSchemaPlus(configschema.NestingMap), 3487 ExpectedOutput: ` # test_instance.example will be updated in-place 3488 ~ resource "test_instance" "example" { 3489 ~ ami = "ami-BEFORE" -> "ami-AFTER" 3490 ~ disks = { 3491 + "disk_a" = { 3492 + mount_point = (sensitive) 3493 + size = "50GB" 3494 }, 3495 } 3496 id = "i-02ae66f368e8518a9" 3497 3498 # (1 unchanged block hidden) 3499 } 3500`, 3501 }, 3502 } 3503 runTestCases(t, testCases) 3504} 3505 3506func TestResourceChange_sensitiveVariable(t *testing.T) { 3507 testCases := map[string]testCase{ 3508 "creation": { 3509 Action: plans.Create, 3510 Mode: addrs.ManagedResourceMode, 3511 Before: cty.NullVal(cty.EmptyObject), 3512 After: cty.ObjectVal(map[string]cty.Value{ 3513 "id": cty.StringVal("i-02ae66f368e8518a9"), 3514 "ami": cty.StringVal("ami-123"), 3515 "map_key": cty.MapVal(map[string]cty.Value{ 3516 "breakfast": cty.NumberIntVal(800), 3517 "dinner": cty.NumberIntVal(2000), 3518 }), 3519 "map_whole": cty.MapVal(map[string]cty.Value{ 3520 "breakfast": cty.StringVal("pizza"), 3521 "dinner": cty.StringVal("pizza"), 3522 }), 3523 "list_field": cty.ListVal([]cty.Value{ 3524 cty.StringVal("hello"), 3525 cty.StringVal("friends"), 3526 cty.StringVal("!"), 3527 }), 3528 "nested_block_list": cty.ListVal([]cty.Value{ 3529 cty.ObjectVal(map[string]cty.Value{ 3530 "an_attr": cty.StringVal("secretval"), 3531 "another": cty.StringVal("not secret"), 3532 }), 3533 }), 3534 "nested_block_set": cty.ListVal([]cty.Value{ 3535 cty.ObjectVal(map[string]cty.Value{ 3536 "an_attr": cty.StringVal("secretval"), 3537 "another": cty.StringVal("not secret"), 3538 }), 3539 }), 3540 }), 3541 AfterValMarks: []cty.PathValueMarks{ 3542 { 3543 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 3544 Marks: cty.NewValueMarks("sensitive"), 3545 }, 3546 { 3547 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, 3548 Marks: cty.NewValueMarks("sensitive"), 3549 }, 3550 { 3551 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 3552 Marks: cty.NewValueMarks("sensitive"), 3553 }, 3554 { 3555 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 3556 Marks: cty.NewValueMarks("sensitive"), 3557 }, 3558 { 3559 // Nested blocks/sets will mark the whole set/block as sensitive 3560 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_list"}}, 3561 Marks: cty.NewValueMarks("sensitive"), 3562 }, 3563 { 3564 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 3565 Marks: cty.NewValueMarks("sensitive"), 3566 }, 3567 }, 3568 RequiredReplace: cty.NewPathSet(), 3569 Schema: &configschema.Block{ 3570 Attributes: map[string]*configschema.Attribute{ 3571 "id": {Type: cty.String, Optional: true, Computed: true}, 3572 "ami": {Type: cty.String, Optional: true}, 3573 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 3574 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 3575 "list_field": {Type: cty.List(cty.String), Optional: true}, 3576 }, 3577 BlockTypes: map[string]*configschema.NestedBlock{ 3578 "nested_block_list": { 3579 Block: configschema.Block{ 3580 Attributes: map[string]*configschema.Attribute{ 3581 "an_attr": {Type: cty.String, Optional: true}, 3582 "another": {Type: cty.String, Optional: true}, 3583 }, 3584 }, 3585 Nesting: configschema.NestingList, 3586 }, 3587 "nested_block_set": { 3588 Block: configschema.Block{ 3589 Attributes: map[string]*configschema.Attribute{ 3590 "an_attr": {Type: cty.String, Optional: true}, 3591 "another": {Type: cty.String, Optional: true}, 3592 }, 3593 }, 3594 Nesting: configschema.NestingSet, 3595 }, 3596 }, 3597 }, 3598 ExpectedOutput: ` # test_instance.example will be created 3599 + resource "test_instance" "example" { 3600 + ami = (sensitive) 3601 + id = "i-02ae66f368e8518a9" 3602 + list_field = [ 3603 + "hello", 3604 + (sensitive), 3605 + "!", 3606 ] 3607 + map_key = { 3608 + "breakfast" = 800 3609 + "dinner" = (sensitive) 3610 } 3611 + map_whole = (sensitive) 3612 3613 + nested_block_list { 3614 # At least one attribute in this block is (or was) sensitive, 3615 # so its contents will not be displayed. 3616 } 3617 3618 + nested_block_set { 3619 # At least one attribute in this block is (or was) sensitive, 3620 # so its contents will not be displayed. 3621 } 3622 } 3623`, 3624 }, 3625 "in-place update - before sensitive": { 3626 Action: plans.Update, 3627 Mode: addrs.ManagedResourceMode, 3628 Before: cty.ObjectVal(map[string]cty.Value{ 3629 "id": cty.StringVal("i-02ae66f368e8518a9"), 3630 "ami": cty.StringVal("ami-BEFORE"), 3631 "special": cty.BoolVal(true), 3632 "some_number": cty.NumberIntVal(1), 3633 "list_field": cty.ListVal([]cty.Value{ 3634 cty.StringVal("hello"), 3635 cty.StringVal("friends"), 3636 cty.StringVal("!"), 3637 }), 3638 "map_key": cty.MapVal(map[string]cty.Value{ 3639 "breakfast": cty.NumberIntVal(800), 3640 "dinner": cty.NumberIntVal(2000), // sensitive key 3641 }), 3642 "map_whole": cty.MapVal(map[string]cty.Value{ 3643 "breakfast": cty.StringVal("pizza"), 3644 "dinner": cty.StringVal("pizza"), 3645 }), 3646 "nested_block": cty.ListVal([]cty.Value{ 3647 cty.ObjectVal(map[string]cty.Value{ 3648 "an_attr": cty.StringVal("secretval"), 3649 }), 3650 }), 3651 "nested_block_set": cty.ListVal([]cty.Value{ 3652 cty.ObjectVal(map[string]cty.Value{ 3653 "an_attr": cty.StringVal("secretval"), 3654 }), 3655 }), 3656 }), 3657 After: cty.ObjectVal(map[string]cty.Value{ 3658 "id": cty.StringVal("i-02ae66f368e8518a9"), 3659 "ami": cty.StringVal("ami-AFTER"), 3660 "special": cty.BoolVal(false), 3661 "some_number": cty.NumberIntVal(2), 3662 "list_field": cty.ListVal([]cty.Value{ 3663 cty.StringVal("hello"), 3664 cty.StringVal("friends"), 3665 cty.StringVal("."), 3666 }), 3667 "map_key": cty.MapVal(map[string]cty.Value{ 3668 "breakfast": cty.NumberIntVal(800), 3669 "dinner": cty.NumberIntVal(1900), 3670 }), 3671 "map_whole": cty.MapVal(map[string]cty.Value{ 3672 "breakfast": cty.StringVal("cereal"), 3673 "dinner": cty.StringVal("pizza"), 3674 }), 3675 "nested_block": cty.ListVal([]cty.Value{ 3676 cty.ObjectVal(map[string]cty.Value{ 3677 "an_attr": cty.StringVal("changed"), 3678 }), 3679 }), 3680 "nested_block_set": cty.ListVal([]cty.Value{ 3681 cty.ObjectVal(map[string]cty.Value{ 3682 "an_attr": cty.StringVal("changed"), 3683 }), 3684 }), 3685 }), 3686 BeforeValMarks: []cty.PathValueMarks{ 3687 { 3688 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 3689 Marks: cty.NewValueMarks("sensitive"), 3690 }, 3691 { 3692 Path: cty.Path{cty.GetAttrStep{Name: "special"}}, 3693 Marks: cty.NewValueMarks("sensitive"), 3694 }, 3695 { 3696 Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, 3697 Marks: cty.NewValueMarks("sensitive"), 3698 }, 3699 { 3700 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, 3701 Marks: cty.NewValueMarks("sensitive"), 3702 }, 3703 { 3704 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 3705 Marks: cty.NewValueMarks("sensitive"), 3706 }, 3707 { 3708 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 3709 Marks: cty.NewValueMarks("sensitive"), 3710 }, 3711 { 3712 Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, 3713 Marks: cty.NewValueMarks("sensitive"), 3714 }, 3715 { 3716 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 3717 Marks: cty.NewValueMarks("sensitive"), 3718 }, 3719 }, 3720 RequiredReplace: cty.NewPathSet(), 3721 Schema: &configschema.Block{ 3722 Attributes: map[string]*configschema.Attribute{ 3723 "id": {Type: cty.String, Optional: true, Computed: true}, 3724 "ami": {Type: cty.String, Optional: true}, 3725 "list_field": {Type: cty.List(cty.String), Optional: true}, 3726 "special": {Type: cty.Bool, Optional: true}, 3727 "some_number": {Type: cty.Number, Optional: true}, 3728 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 3729 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 3730 }, 3731 BlockTypes: map[string]*configschema.NestedBlock{ 3732 "nested_block": { 3733 Block: configschema.Block{ 3734 Attributes: map[string]*configschema.Attribute{ 3735 "an_attr": {Type: cty.String, Optional: true}, 3736 }, 3737 }, 3738 Nesting: configschema.NestingList, 3739 }, 3740 "nested_block_set": { 3741 Block: configschema.Block{ 3742 Attributes: map[string]*configschema.Attribute{ 3743 "an_attr": {Type: cty.String, Optional: true}, 3744 }, 3745 }, 3746 Nesting: configschema.NestingSet, 3747 }, 3748 }, 3749 }, 3750 ExpectedOutput: ` # test_instance.example will be updated in-place 3751 ~ resource "test_instance" "example" { 3752 # Warning: this attribute value will no longer be marked as sensitive 3753 # after applying this change. 3754 ~ ami = (sensitive) 3755 id = "i-02ae66f368e8518a9" 3756 ~ list_field = [ 3757 # (1 unchanged element hidden) 3758 "friends", 3759 - (sensitive), 3760 + ".", 3761 ] 3762 ~ map_key = { 3763 # Warning: this attribute value will no longer be marked as sensitive 3764 # after applying this change. 3765 ~ "dinner" = (sensitive) 3766 # (1 unchanged element hidden) 3767 } 3768 # Warning: this attribute value will no longer be marked as sensitive 3769 # after applying this change. 3770 ~ map_whole = (sensitive) 3771 # Warning: this attribute value will no longer be marked as sensitive 3772 # after applying this change. 3773 ~ some_number = (sensitive) 3774 # Warning: this attribute value will no longer be marked as sensitive 3775 # after applying this change. 3776 ~ special = (sensitive) 3777 3778 # Warning: this block will no longer be marked as sensitive 3779 # after applying this change. 3780 ~ nested_block { 3781 # At least one attribute in this block is (or was) sensitive, 3782 # so its contents will not be displayed. 3783 } 3784 3785 # Warning: this block will no longer be marked as sensitive 3786 # after applying this change. 3787 ~ nested_block_set { 3788 # At least one attribute in this block is (or was) sensitive, 3789 # so its contents will not be displayed. 3790 } 3791 } 3792`, 3793 }, 3794 "in-place update - after sensitive": { 3795 Action: plans.Update, 3796 Mode: addrs.ManagedResourceMode, 3797 Before: cty.ObjectVal(map[string]cty.Value{ 3798 "id": cty.StringVal("i-02ae66f368e8518a9"), 3799 "list_field": cty.ListVal([]cty.Value{ 3800 cty.StringVal("hello"), 3801 cty.StringVal("friends"), 3802 }), 3803 "map_key": cty.MapVal(map[string]cty.Value{ 3804 "breakfast": cty.NumberIntVal(800), 3805 "dinner": cty.NumberIntVal(2000), // sensitive key 3806 }), 3807 "map_whole": cty.MapVal(map[string]cty.Value{ 3808 "breakfast": cty.StringVal("pizza"), 3809 "dinner": cty.StringVal("pizza"), 3810 }), 3811 "nested_block_single": cty.ObjectVal(map[string]cty.Value{ 3812 "an_attr": cty.StringVal("original"), 3813 }), 3814 }), 3815 After: cty.ObjectVal(map[string]cty.Value{ 3816 "id": cty.StringVal("i-02ae66f368e8518a9"), 3817 "list_field": cty.ListVal([]cty.Value{ 3818 cty.StringVal("goodbye"), 3819 cty.StringVal("friends"), 3820 }), 3821 "map_key": cty.MapVal(map[string]cty.Value{ 3822 "breakfast": cty.NumberIntVal(700), 3823 "dinner": cty.NumberIntVal(2100), // sensitive key 3824 }), 3825 "map_whole": cty.MapVal(map[string]cty.Value{ 3826 "breakfast": cty.StringVal("cereal"), 3827 "dinner": cty.StringVal("pizza"), 3828 }), 3829 "nested_block_single": cty.ObjectVal(map[string]cty.Value{ 3830 "an_attr": cty.StringVal("changed"), 3831 }), 3832 }), 3833 AfterValMarks: []cty.PathValueMarks{ 3834 { 3835 Path: cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}}, 3836 Marks: cty.NewValueMarks("sensitive"), 3837 }, 3838 { 3839 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, 3840 Marks: cty.NewValueMarks("sensitive"), 3841 }, 3842 { 3843 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 3844 Marks: cty.NewValueMarks("sensitive"), 3845 }, 3846 { 3847 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 3848 Marks: cty.NewValueMarks("sensitive"), 3849 }, 3850 { 3851 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_single"}}, 3852 Marks: cty.NewValueMarks("sensitive"), 3853 }, 3854 }, 3855 RequiredReplace: cty.NewPathSet(), 3856 Schema: &configschema.Block{ 3857 Attributes: map[string]*configschema.Attribute{ 3858 "id": {Type: cty.String, Optional: true, Computed: true}, 3859 "list_field": {Type: cty.List(cty.String), Optional: true}, 3860 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 3861 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 3862 }, 3863 BlockTypes: map[string]*configschema.NestedBlock{ 3864 "nested_block_single": { 3865 Block: configschema.Block{ 3866 Attributes: map[string]*configschema.Attribute{ 3867 "an_attr": {Type: cty.String, Optional: true}, 3868 }, 3869 }, 3870 Nesting: configschema.NestingSingle, 3871 }, 3872 }, 3873 }, 3874 ExpectedOutput: ` # test_instance.example will be updated in-place 3875 ~ resource "test_instance" "example" { 3876 id = "i-02ae66f368e8518a9" 3877 ~ list_field = [ 3878 - "hello", 3879 + (sensitive), 3880 "friends", 3881 ] 3882 ~ map_key = { 3883 ~ "breakfast" = 800 -> 700 3884 # Warning: this attribute value will be marked as sensitive and will not 3885 # display in UI output after applying this change. 3886 ~ "dinner" = (sensitive) 3887 } 3888 # Warning: this attribute value will be marked as sensitive and will not 3889 # display in UI output after applying this change. 3890 ~ map_whole = (sensitive) 3891 3892 # Warning: this block will be marked as sensitive and will not 3893 # display in UI output after applying this change. 3894 ~ nested_block_single { 3895 # At least one attribute in this block is (or was) sensitive, 3896 # so its contents will not be displayed. 3897 } 3898 } 3899`, 3900 }, 3901 "in-place update - both sensitive": { 3902 Action: plans.Update, 3903 Mode: addrs.ManagedResourceMode, 3904 Before: cty.ObjectVal(map[string]cty.Value{ 3905 "id": cty.StringVal("i-02ae66f368e8518a9"), 3906 "ami": cty.StringVal("ami-BEFORE"), 3907 "list_field": cty.ListVal([]cty.Value{ 3908 cty.StringVal("hello"), 3909 cty.StringVal("friends"), 3910 }), 3911 "map_key": cty.MapVal(map[string]cty.Value{ 3912 "breakfast": cty.NumberIntVal(800), 3913 "dinner": cty.NumberIntVal(2000), // sensitive key 3914 }), 3915 "map_whole": cty.MapVal(map[string]cty.Value{ 3916 "breakfast": cty.StringVal("pizza"), 3917 "dinner": cty.StringVal("pizza"), 3918 }), 3919 "nested_block_map": cty.MapVal(map[string]cty.Value{ 3920 "foo": cty.ObjectVal(map[string]cty.Value{ 3921 "an_attr": cty.StringVal("original"), 3922 }), 3923 }), 3924 }), 3925 After: cty.ObjectVal(map[string]cty.Value{ 3926 "id": cty.StringVal("i-02ae66f368e8518a9"), 3927 "ami": cty.StringVal("ami-AFTER"), 3928 "list_field": cty.ListVal([]cty.Value{ 3929 cty.StringVal("goodbye"), 3930 cty.StringVal("friends"), 3931 }), 3932 "map_key": cty.MapVal(map[string]cty.Value{ 3933 "breakfast": cty.NumberIntVal(800), 3934 "dinner": cty.NumberIntVal(1800), // sensitive key 3935 }), 3936 "map_whole": cty.MapVal(map[string]cty.Value{ 3937 "breakfast": cty.StringVal("cereal"), 3938 "dinner": cty.StringVal("pizza"), 3939 }), 3940 "nested_block_map": cty.MapVal(map[string]cty.Value{ 3941 "foo": cty.ObjectVal(map[string]cty.Value{ 3942 "an_attr": cty.UnknownVal(cty.String), 3943 }), 3944 }), 3945 }), 3946 BeforeValMarks: []cty.PathValueMarks{ 3947 { 3948 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 3949 Marks: cty.NewValueMarks("sensitive"), 3950 }, 3951 { 3952 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, 3953 Marks: cty.NewValueMarks("sensitive"), 3954 }, 3955 { 3956 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 3957 Marks: cty.NewValueMarks("sensitive"), 3958 }, 3959 { 3960 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 3961 Marks: cty.NewValueMarks("sensitive"), 3962 }, 3963 { 3964 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, 3965 Marks: cty.NewValueMarks("sensitive"), 3966 }, 3967 }, 3968 AfterValMarks: []cty.PathValueMarks{ 3969 { 3970 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 3971 Marks: cty.NewValueMarks("sensitive"), 3972 }, 3973 { 3974 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, 3975 Marks: cty.NewValueMarks("sensitive"), 3976 }, 3977 { 3978 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 3979 Marks: cty.NewValueMarks("sensitive"), 3980 }, 3981 { 3982 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 3983 Marks: cty.NewValueMarks("sensitive"), 3984 }, 3985 { 3986 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, 3987 Marks: cty.NewValueMarks("sensitive"), 3988 }, 3989 }, 3990 RequiredReplace: cty.NewPathSet(), 3991 Schema: &configschema.Block{ 3992 Attributes: map[string]*configschema.Attribute{ 3993 "id": {Type: cty.String, Optional: true, Computed: true}, 3994 "ami": {Type: cty.String, Optional: true}, 3995 "list_field": {Type: cty.List(cty.String), Optional: true}, 3996 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 3997 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 3998 }, 3999 BlockTypes: map[string]*configschema.NestedBlock{ 4000 "nested_block_map": { 4001 Block: configschema.Block{ 4002 Attributes: map[string]*configschema.Attribute{ 4003 "an_attr": {Type: cty.String, Optional: true}, 4004 }, 4005 }, 4006 Nesting: configschema.NestingMap, 4007 }, 4008 }, 4009 }, 4010 ExpectedOutput: ` # test_instance.example will be updated in-place 4011 ~ resource "test_instance" "example" { 4012 ~ ami = (sensitive) 4013 id = "i-02ae66f368e8518a9" 4014 ~ list_field = [ 4015 - (sensitive), 4016 + (sensitive), 4017 "friends", 4018 ] 4019 ~ map_key = { 4020 ~ "dinner" = (sensitive) 4021 # (1 unchanged element hidden) 4022 } 4023 ~ map_whole = (sensitive) 4024 4025 ~ nested_block_map { 4026 # At least one attribute in this block is (or was) sensitive, 4027 # so its contents will not be displayed. 4028 } 4029 } 4030`, 4031 }, 4032 "in-place update - value unchanged, sensitivity changes": { 4033 Action: plans.Update, 4034 Mode: addrs.ManagedResourceMode, 4035 Before: cty.ObjectVal(map[string]cty.Value{ 4036 "id": cty.StringVal("i-02ae66f368e8518a9"), 4037 "ami": cty.StringVal("ami-BEFORE"), 4038 "special": cty.BoolVal(true), 4039 "some_number": cty.NumberIntVal(1), 4040 "list_field": cty.ListVal([]cty.Value{ 4041 cty.StringVal("hello"), 4042 cty.StringVal("friends"), 4043 cty.StringVal("!"), 4044 }), 4045 "map_key": cty.MapVal(map[string]cty.Value{ 4046 "breakfast": cty.NumberIntVal(800), 4047 "dinner": cty.NumberIntVal(2000), // sensitive key 4048 }), 4049 "map_whole": cty.MapVal(map[string]cty.Value{ 4050 "breakfast": cty.StringVal("pizza"), 4051 "dinner": cty.StringVal("pizza"), 4052 }), 4053 "nested_block": cty.ListVal([]cty.Value{ 4054 cty.ObjectVal(map[string]cty.Value{ 4055 "an_attr": cty.StringVal("secretval"), 4056 }), 4057 }), 4058 "nested_block_set": cty.ListVal([]cty.Value{ 4059 cty.ObjectVal(map[string]cty.Value{ 4060 "an_attr": cty.StringVal("secretval"), 4061 }), 4062 }), 4063 }), 4064 After: cty.ObjectVal(map[string]cty.Value{ 4065 "id": cty.StringVal("i-02ae66f368e8518a9"), 4066 "ami": cty.StringVal("ami-BEFORE"), 4067 "special": cty.BoolVal(true), 4068 "some_number": cty.NumberIntVal(1), 4069 "list_field": cty.ListVal([]cty.Value{ 4070 cty.StringVal("hello"), 4071 cty.StringVal("friends"), 4072 cty.StringVal("!"), 4073 }), 4074 "map_key": cty.MapVal(map[string]cty.Value{ 4075 "breakfast": cty.NumberIntVal(800), 4076 "dinner": cty.NumberIntVal(2000), // sensitive key 4077 }), 4078 "map_whole": cty.MapVal(map[string]cty.Value{ 4079 "breakfast": cty.StringVal("pizza"), 4080 "dinner": cty.StringVal("pizza"), 4081 }), 4082 "nested_block": cty.ListVal([]cty.Value{ 4083 cty.ObjectVal(map[string]cty.Value{ 4084 "an_attr": cty.StringVal("secretval"), 4085 }), 4086 }), 4087 "nested_block_set": cty.ListVal([]cty.Value{ 4088 cty.ObjectVal(map[string]cty.Value{ 4089 "an_attr": cty.StringVal("secretval"), 4090 }), 4091 }), 4092 }), 4093 BeforeValMarks: []cty.PathValueMarks{ 4094 { 4095 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 4096 Marks: cty.NewValueMarks("sensitive"), 4097 }, 4098 { 4099 Path: cty.Path{cty.GetAttrStep{Name: "special"}}, 4100 Marks: cty.NewValueMarks("sensitive"), 4101 }, 4102 { 4103 Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, 4104 Marks: cty.NewValueMarks("sensitive"), 4105 }, 4106 { 4107 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, 4108 Marks: cty.NewValueMarks("sensitive"), 4109 }, 4110 { 4111 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 4112 Marks: cty.NewValueMarks("sensitive"), 4113 }, 4114 { 4115 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 4116 Marks: cty.NewValueMarks("sensitive"), 4117 }, 4118 { 4119 Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, 4120 Marks: cty.NewValueMarks("sensitive"), 4121 }, 4122 { 4123 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 4124 Marks: cty.NewValueMarks("sensitive"), 4125 }, 4126 }, 4127 RequiredReplace: cty.NewPathSet(), 4128 Schema: &configschema.Block{ 4129 Attributes: map[string]*configschema.Attribute{ 4130 "id": {Type: cty.String, Optional: true, Computed: true}, 4131 "ami": {Type: cty.String, Optional: true}, 4132 "list_field": {Type: cty.List(cty.String), Optional: true}, 4133 "special": {Type: cty.Bool, Optional: true}, 4134 "some_number": {Type: cty.Number, Optional: true}, 4135 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 4136 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 4137 }, 4138 BlockTypes: map[string]*configschema.NestedBlock{ 4139 "nested_block": { 4140 Block: configschema.Block{ 4141 Attributes: map[string]*configschema.Attribute{ 4142 "an_attr": {Type: cty.String, Optional: true}, 4143 }, 4144 }, 4145 Nesting: configschema.NestingList, 4146 }, 4147 "nested_block_set": { 4148 Block: configschema.Block{ 4149 Attributes: map[string]*configschema.Attribute{ 4150 "an_attr": {Type: cty.String, Optional: true}, 4151 }, 4152 }, 4153 Nesting: configschema.NestingSet, 4154 }, 4155 }, 4156 }, 4157 ExpectedOutput: ` # test_instance.example will be updated in-place 4158 ~ resource "test_instance" "example" { 4159 # Warning: this attribute value will no longer be marked as sensitive 4160 # after applying this change. The value is unchanged. 4161 ~ ami = (sensitive) 4162 id = "i-02ae66f368e8518a9" 4163 ~ list_field = [ 4164 # (1 unchanged element hidden) 4165 "friends", 4166 - (sensitive), 4167 + "!", 4168 ] 4169 ~ map_key = { 4170 # Warning: this attribute value will no longer be marked as sensitive 4171 # after applying this change. The value is unchanged. 4172 ~ "dinner" = (sensitive) 4173 # (1 unchanged element hidden) 4174 } 4175 # Warning: this attribute value will no longer be marked as sensitive 4176 # after applying this change. The value is unchanged. 4177 ~ map_whole = (sensitive) 4178 # Warning: this attribute value will no longer be marked as sensitive 4179 # after applying this change. The value is unchanged. 4180 ~ some_number = (sensitive) 4181 # Warning: this attribute value will no longer be marked as sensitive 4182 # after applying this change. The value is unchanged. 4183 ~ special = (sensitive) 4184 4185 # Warning: this block will no longer be marked as sensitive 4186 # after applying this change. 4187 ~ nested_block { 4188 # At least one attribute in this block is (or was) sensitive, 4189 # so its contents will not be displayed. 4190 } 4191 4192 # Warning: this block will no longer be marked as sensitive 4193 # after applying this change. 4194 ~ nested_block_set { 4195 # At least one attribute in this block is (or was) sensitive, 4196 # so its contents will not be displayed. 4197 } 4198 } 4199`, 4200 }, 4201 "deletion": { 4202 Action: plans.Delete, 4203 Mode: addrs.ManagedResourceMode, 4204 Before: cty.ObjectVal(map[string]cty.Value{ 4205 "id": cty.StringVal("i-02ae66f368e8518a9"), 4206 "ami": cty.StringVal("ami-BEFORE"), 4207 "list_field": cty.ListVal([]cty.Value{ 4208 cty.StringVal("hello"), 4209 cty.StringVal("friends"), 4210 }), 4211 "map_key": cty.MapVal(map[string]cty.Value{ 4212 "breakfast": cty.NumberIntVal(800), 4213 "dinner": cty.NumberIntVal(2000), // sensitive key 4214 }), 4215 "map_whole": cty.MapVal(map[string]cty.Value{ 4216 "breakfast": cty.StringVal("pizza"), 4217 "dinner": cty.StringVal("pizza"), 4218 }), 4219 "nested_block": cty.ListVal([]cty.Value{ 4220 cty.ObjectVal(map[string]cty.Value{ 4221 "an_attr": cty.StringVal("secret"), 4222 "another": cty.StringVal("not secret"), 4223 }), 4224 }), 4225 "nested_block_set": cty.ListVal([]cty.Value{ 4226 cty.ObjectVal(map[string]cty.Value{ 4227 "an_attr": cty.StringVal("secret"), 4228 "another": cty.StringVal("not secret"), 4229 }), 4230 }), 4231 }), 4232 After: cty.NullVal(cty.EmptyObject), 4233 BeforeValMarks: []cty.PathValueMarks{ 4234 { 4235 Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, 4236 Marks: cty.NewValueMarks("sensitive"), 4237 }, 4238 { 4239 Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, 4240 Marks: cty.NewValueMarks("sensitive"), 4241 }, 4242 { 4243 Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, 4244 Marks: cty.NewValueMarks("sensitive"), 4245 }, 4246 { 4247 Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, 4248 Marks: cty.NewValueMarks("sensitive"), 4249 }, 4250 { 4251 Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, 4252 Marks: cty.NewValueMarks("sensitive"), 4253 }, 4254 { 4255 Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, 4256 Marks: cty.NewValueMarks("sensitive"), 4257 }, 4258 }, 4259 RequiredReplace: cty.NewPathSet(), 4260 Schema: &configschema.Block{ 4261 Attributes: map[string]*configschema.Attribute{ 4262 "id": {Type: cty.String, Optional: true, Computed: true}, 4263 "ami": {Type: cty.String, Optional: true}, 4264 "list_field": {Type: cty.List(cty.String), Optional: true}, 4265 "map_key": {Type: cty.Map(cty.Number), Optional: true}, 4266 "map_whole": {Type: cty.Map(cty.String), Optional: true}, 4267 }, 4268 BlockTypes: map[string]*configschema.NestedBlock{ 4269 "nested_block_set": { 4270 Block: configschema.Block{ 4271 Attributes: map[string]*configschema.Attribute{ 4272 "an_attr": {Type: cty.String, Optional: true}, 4273 "another": {Type: cty.String, Optional: true}, 4274 }, 4275 }, 4276 Nesting: configschema.NestingSet, 4277 }, 4278 }, 4279 }, 4280 ExpectedOutput: ` # test_instance.example will be destroyed 4281 - resource "test_instance" "example" { 4282 - ami = (sensitive) -> null 4283 - id = "i-02ae66f368e8518a9" -> null 4284 - list_field = [ 4285 - "hello", 4286 - (sensitive), 4287 ] -> null 4288 - map_key = { 4289 - "breakfast" = 800 4290 - "dinner" = (sensitive) 4291 } -> null 4292 - map_whole = (sensitive) -> null 4293 4294 - nested_block_set { 4295 # At least one attribute in this block is (or was) sensitive, 4296 # so its contents will not be displayed. 4297 } 4298 } 4299`, 4300 }, 4301 "update with sensitive value forcing replacement": { 4302 Action: plans.DeleteThenCreate, 4303 Mode: addrs.ManagedResourceMode, 4304 Before: cty.ObjectVal(map[string]cty.Value{ 4305 "id": cty.StringVal("i-02ae66f368e8518a9"), 4306 "ami": cty.StringVal("ami-BEFORE"), 4307 "nested_block_set": cty.SetVal([]cty.Value{ 4308 cty.ObjectVal(map[string]cty.Value{ 4309 "an_attr": cty.StringVal("secret"), 4310 }), 4311 }), 4312 }), 4313 After: cty.ObjectVal(map[string]cty.Value{ 4314 "id": cty.StringVal("i-02ae66f368e8518a9"), 4315 "ami": cty.StringVal("ami-AFTER"), 4316 "nested_block_set": cty.SetVal([]cty.Value{ 4317 cty.ObjectVal(map[string]cty.Value{ 4318 "an_attr": cty.StringVal("changed"), 4319 }), 4320 }), 4321 }), 4322 BeforeValMarks: []cty.PathValueMarks{ 4323 { 4324 Path: cty.GetAttrPath("ami"), 4325 Marks: cty.NewValueMarks("sensitive"), 4326 }, 4327 { 4328 Path: cty.GetAttrPath("nested_block_set"), 4329 Marks: cty.NewValueMarks("sensitive"), 4330 }, 4331 }, 4332 AfterValMarks: []cty.PathValueMarks{ 4333 { 4334 Path: cty.GetAttrPath("ami"), 4335 Marks: cty.NewValueMarks("sensitive"), 4336 }, 4337 { 4338 Path: cty.GetAttrPath("nested_block_set"), 4339 Marks: cty.NewValueMarks("sensitive"), 4340 }, 4341 }, 4342 Schema: &configschema.Block{ 4343 Attributes: map[string]*configschema.Attribute{ 4344 "id": {Type: cty.String, Optional: true, Computed: true}, 4345 "ami": {Type: cty.String, Optional: true}, 4346 }, 4347 BlockTypes: map[string]*configschema.NestedBlock{ 4348 "nested_block_set": { 4349 Block: configschema.Block{ 4350 Attributes: map[string]*configschema.Attribute{ 4351 "an_attr": {Type: cty.String, Required: true}, 4352 }, 4353 }, 4354 Nesting: configschema.NestingSet, 4355 }, 4356 }, 4357 }, 4358 RequiredReplace: cty.NewPathSet( 4359 cty.GetAttrPath("ami"), 4360 cty.GetAttrPath("nested_block_set"), 4361 ), 4362 ExpectedOutput: ` # test_instance.example must be replaced 4363-/+ resource "test_instance" "example" { 4364 ~ ami = (sensitive) # forces replacement 4365 id = "i-02ae66f368e8518a9" 4366 4367 ~ nested_block_set { # forces replacement 4368 # At least one attribute in this block is (or was) sensitive, 4369 # so its contents will not be displayed. 4370 } 4371 } 4372`, 4373 }, 4374 "update with sensitive attribute forcing replacement": { 4375 Action: plans.DeleteThenCreate, 4376 Mode: addrs.ManagedResourceMode, 4377 Before: cty.ObjectVal(map[string]cty.Value{ 4378 "id": cty.StringVal("i-02ae66f368e8518a9"), 4379 "ami": cty.StringVal("ami-BEFORE"), 4380 }), 4381 After: cty.ObjectVal(map[string]cty.Value{ 4382 "id": cty.StringVal("i-02ae66f368e8518a9"), 4383 "ami": cty.StringVal("ami-AFTER"), 4384 }), 4385 Schema: &configschema.Block{ 4386 Attributes: map[string]*configschema.Attribute{ 4387 "id": {Type: cty.String, Optional: true, Computed: true}, 4388 "ami": {Type: cty.String, Optional: true, Computed: true, Sensitive: true}, 4389 }, 4390 }, 4391 RequiredReplace: cty.NewPathSet( 4392 cty.GetAttrPath("ami"), 4393 ), 4394 ExpectedOutput: ` # test_instance.example must be replaced 4395-/+ resource "test_instance" "example" { 4396 ~ ami = (sensitive value) # forces replacement 4397 id = "i-02ae66f368e8518a9" 4398 } 4399`, 4400 }, 4401 "update with sensitive nested type attribute forcing replacement": { 4402 Action: plans.DeleteThenCreate, 4403 Mode: addrs.ManagedResourceMode, 4404 Before: cty.ObjectVal(map[string]cty.Value{ 4405 "id": cty.StringVal("i-02ae66f368e8518a9"), 4406 "conn_info": cty.ObjectVal(map[string]cty.Value{ 4407 "user": cty.StringVal("not-secret"), 4408 "password": cty.StringVal("top-secret"), 4409 }), 4410 }), 4411 After: cty.ObjectVal(map[string]cty.Value{ 4412 "id": cty.StringVal("i-02ae66f368e8518a9"), 4413 "conn_info": cty.ObjectVal(map[string]cty.Value{ 4414 "user": cty.StringVal("not-secret"), 4415 "password": cty.StringVal("new-secret"), 4416 }), 4417 }), 4418 Schema: &configschema.Block{ 4419 Attributes: map[string]*configschema.Attribute{ 4420 "id": {Type: cty.String, Optional: true, Computed: true}, 4421 "conn_info": { 4422 NestedType: &configschema.Object{ 4423 Nesting: configschema.NestingSingle, 4424 Attributes: map[string]*configschema.Attribute{ 4425 "user": {Type: cty.String, Optional: true}, 4426 "password": {Type: cty.String, Optional: true, Sensitive: true}, 4427 }, 4428 }, 4429 }, 4430 }, 4431 }, 4432 RequiredReplace: cty.NewPathSet( 4433 cty.GetAttrPath("conn_info"), 4434 cty.GetAttrPath("password"), 4435 ), 4436 ExpectedOutput: ` # test_instance.example must be replaced 4437-/+ resource "test_instance" "example" { 4438 ~ conn_info = { # forces replacement 4439 ~ password = (sensitive value) 4440 # (1 unchanged attribute hidden) 4441 } 4442 id = "i-02ae66f368e8518a9" 4443 } 4444`, 4445 }, 4446 } 4447 runTestCases(t, testCases) 4448} 4449 4450type testCase struct { 4451 Action plans.Action 4452 ActionReason plans.ResourceInstanceChangeActionReason 4453 Mode addrs.ResourceMode 4454 DeposedKey states.DeposedKey 4455 Before cty.Value 4456 BeforeValMarks []cty.PathValueMarks 4457 AfterValMarks []cty.PathValueMarks 4458 After cty.Value 4459 Schema *configschema.Block 4460 RequiredReplace cty.PathSet 4461 ExpectedOutput string 4462} 4463 4464func runTestCases(t *testing.T, testCases map[string]testCase) { 4465 color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} 4466 4467 for name, tc := range testCases { 4468 t.Run(name, func(t *testing.T) { 4469 ty := tc.Schema.ImpliedType() 4470 4471 beforeVal := tc.Before 4472 switch { // Some fixups to make the test cases a little easier to write 4473 case beforeVal.IsNull(): 4474 beforeVal = cty.NullVal(ty) // allow mistyped nulls 4475 case !beforeVal.IsKnown(): 4476 beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns 4477 } 4478 before, err := plans.NewDynamicValue(beforeVal, ty) 4479 if err != nil { 4480 t.Fatal(err) 4481 } 4482 4483 afterVal := tc.After 4484 switch { // Some fixups to make the test cases a little easier to write 4485 case afterVal.IsNull(): 4486 afterVal = cty.NullVal(ty) // allow mistyped nulls 4487 case !afterVal.IsKnown(): 4488 afterVal = cty.UnknownVal(ty) // allow mistyped unknowns 4489 } 4490 after, err := plans.NewDynamicValue(afterVal, ty) 4491 if err != nil { 4492 t.Fatal(err) 4493 } 4494 4495 change := &plans.ResourceInstanceChangeSrc{ 4496 Addr: addrs.Resource{ 4497 Mode: tc.Mode, 4498 Type: "test_instance", 4499 Name: "example", 4500 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 4501 DeposedKey: tc.DeposedKey, 4502 ProviderAddr: addrs.AbsProviderConfig{ 4503 Provider: addrs.NewDefaultProvider("test"), 4504 Module: addrs.RootModule, 4505 }, 4506 ChangeSrc: plans.ChangeSrc{ 4507 Action: tc.Action, 4508 Before: before, 4509 After: after, 4510 BeforeValMarks: tc.BeforeValMarks, 4511 AfterValMarks: tc.AfterValMarks, 4512 }, 4513 ActionReason: tc.ActionReason, 4514 RequiredReplace: tc.RequiredReplace, 4515 } 4516 4517 output := ResourceChange(change, tc.Schema, color) 4518 if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" { 4519 t.Errorf("wrong output\n%s", diff) 4520 } 4521 }) 4522 } 4523} 4524 4525func TestOutputChanges(t *testing.T) { 4526 color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} 4527 4528 testCases := map[string]struct { 4529 changes []*plans.OutputChangeSrc 4530 output string 4531 }{ 4532 "new output value": { 4533 []*plans.OutputChangeSrc{ 4534 outputChange( 4535 "foo", 4536 cty.NullVal(cty.DynamicPseudoType), 4537 cty.StringVal("bar"), 4538 false, 4539 ), 4540 }, 4541 ` 4542 + foo = "bar"`, 4543 }, 4544 "removed output": { 4545 []*plans.OutputChangeSrc{ 4546 outputChange( 4547 "foo", 4548 cty.StringVal("bar"), 4549 cty.NullVal(cty.DynamicPseudoType), 4550 false, 4551 ), 4552 }, 4553 ` 4554 - foo = "bar" -> null`, 4555 }, 4556 "single string change": { 4557 []*plans.OutputChangeSrc{ 4558 outputChange( 4559 "foo", 4560 cty.StringVal("bar"), 4561 cty.StringVal("baz"), 4562 false, 4563 ), 4564 }, 4565 ` 4566 ~ foo = "bar" -> "baz"`, 4567 }, 4568 "element added to list": { 4569 []*plans.OutputChangeSrc{ 4570 outputChange( 4571 "foo", 4572 cty.ListVal([]cty.Value{ 4573 cty.StringVal("alpha"), 4574 cty.StringVal("beta"), 4575 cty.StringVal("delta"), 4576 cty.StringVal("epsilon"), 4577 }), 4578 cty.ListVal([]cty.Value{ 4579 cty.StringVal("alpha"), 4580 cty.StringVal("beta"), 4581 cty.StringVal("gamma"), 4582 cty.StringVal("delta"), 4583 cty.StringVal("epsilon"), 4584 }), 4585 false, 4586 ), 4587 }, 4588 ` 4589 ~ foo = [ 4590 # (1 unchanged element hidden) 4591 "beta", 4592 + "gamma", 4593 "delta", 4594 # (1 unchanged element hidden) 4595 ]`, 4596 }, 4597 "multiple outputs changed, one sensitive": { 4598 []*plans.OutputChangeSrc{ 4599 outputChange( 4600 "a", 4601 cty.NumberIntVal(1), 4602 cty.NumberIntVal(2), 4603 false, 4604 ), 4605 outputChange( 4606 "b", 4607 cty.StringVal("hunter2"), 4608 cty.StringVal("correct-horse-battery-staple"), 4609 true, 4610 ), 4611 outputChange( 4612 "c", 4613 cty.BoolVal(false), 4614 cty.BoolVal(true), 4615 false, 4616 ), 4617 }, 4618 ` 4619 ~ a = 1 -> 2 4620 ~ b = (sensitive value) 4621 ~ c = false -> true`, 4622 }, 4623 } 4624 4625 for name, tc := range testCases { 4626 t.Run(name, func(t *testing.T) { 4627 output := OutputChanges(tc.changes, color) 4628 if output != tc.output { 4629 t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output) 4630 } 4631 }) 4632 } 4633} 4634 4635func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc { 4636 addr := addrs.AbsOutputValue{ 4637 OutputValue: addrs.OutputValue{Name: name}, 4638 } 4639 4640 change := &plans.OutputChange{ 4641 Addr: addr, Change: plans.Change{ 4642 Before: before, 4643 After: after, 4644 }, 4645 Sensitive: sensitive, 4646 } 4647 4648 changeSrc, err := change.Encode() 4649 if err != nil { 4650 panic(fmt.Sprintf("failed to encode change for %s: %s", addr, err)) 4651 } 4652 4653 return changeSrc 4654} 4655 4656// A basic test schema using a configurable NestingMode for one (NestedType) attribute and one block 4657func testSchema(nesting configschema.NestingMode) *configschema.Block { 4658 return &configschema.Block{ 4659 Attributes: map[string]*configschema.Attribute{ 4660 "id": {Type: cty.String, Optional: true, Computed: true}, 4661 "ami": {Type: cty.String, Optional: true}, 4662 "disks": { 4663 NestedType: &configschema.Object{ 4664 Attributes: map[string]*configschema.Attribute{ 4665 "mount_point": {Type: cty.String, Optional: true}, 4666 "size": {Type: cty.String, Optional: true}, 4667 }, 4668 Nesting: nesting, 4669 }, 4670 }, 4671 }, 4672 BlockTypes: map[string]*configschema.NestedBlock{ 4673 "root_block_device": { 4674 Block: configschema.Block{ 4675 Attributes: map[string]*configschema.Attribute{ 4676 "volume_type": { 4677 Type: cty.String, 4678 Optional: true, 4679 Computed: true, 4680 }, 4681 }, 4682 }, 4683 Nesting: nesting, 4684 }, 4685 }, 4686 } 4687} 4688 4689// similar to testSchema with the addition of a "new_field" block 4690func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block { 4691 return &configschema.Block{ 4692 Attributes: map[string]*configschema.Attribute{ 4693 "id": {Type: cty.String, Optional: true, Computed: true}, 4694 "ami": {Type: cty.String, Optional: true}, 4695 "disks": { 4696 NestedType: &configschema.Object{ 4697 Attributes: map[string]*configschema.Attribute{ 4698 "mount_point": {Type: cty.String, Optional: true}, 4699 "size": {Type: cty.String, Optional: true}, 4700 }, 4701 Nesting: nesting, 4702 }, 4703 }, 4704 }, 4705 BlockTypes: map[string]*configschema.NestedBlock{ 4706 "root_block_device": { 4707 Block: configschema.Block{ 4708 Attributes: map[string]*configschema.Attribute{ 4709 "volume_type": { 4710 Type: cty.String, 4711 Optional: true, 4712 Computed: true, 4713 }, 4714 "new_field": { 4715 Type: cty.String, 4716 Optional: true, 4717 Computed: true, 4718 }, 4719 }, 4720 }, 4721 Nesting: nesting, 4722 }, 4723 }, 4724 } 4725} 4726