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